OpenClassrooms
PROJET n°12 : Détectez des faux billets avec R ou Python
  • 1 - INTRODUCTION ET CHARGEMENT DES DONNES

  • 2 - ANALYSE EXPLORATOIRE

    • 2.1 - Analyse descriptive
    • 2.2 - Analyse univariée
    • 2.3 - Analyse bi-variée
    • 2.4 - Matrice de corrélation linéaire (heatmap)
    • 2.5 - Comparaison des groupes de vrais et faux billets
    • 2.6 - Distribution des valeurs manquantes au sein des 2 groupes
    • 2.7 - Analyse des valeurs aberrantes à l'aide du Z-score
  • 3 - TRAITEMENT DES VALEURS MANQUANTES

    • 3.1 - Imputations avec l'ensemble des variables explicatives
    • 3.2 - Imputation avec selection des variables explicatives
  • 4 - MODÉLISATION

    • 4.1 - Préparation, entraînement et test sur les données validation
    • 4.2 - KNN RandomForest Régression logistique

Config notebook¶

In [1]:
# Version de Python :
! python --version
Python 3.9.21
In [ ]:
 
In [2]:
# Imports de base Python
import json  # Pour la manipulation de fichiers JSON
import logging  # Pour la gestion des logs
import os  # Pour l'interaction avec le système d'exploitation
import time  # Pour la mesure du temps d'exécution
import warnings  # Pour la gestion des avertissements
from decimal import Decimal, getcontext, ROUND_HALF_UP  # Pour la manipulation de nombres décimaux

# Configuration optimisée des threads CPU
os.environ["OMP_NUM_THREADS"] = "10"  # Optimisé pour les 10 cœurs physiques

# Imports pour l'analyse numérique et le traitement des données
import numpy as np  # Pour la manipulation de tableaux multidimensionnels
import cupy as cp  # Version GPU de NumPy
import pandas as pd  # Pour la manipulation de données tabulaires
import dask.dataframe as dd  # Pour le traitement de données distribuées
from dask.distributed import Client, LocalCluster  # Pour le calcul distribué

# Configuration GPU pour NVIDIA GTX 1070
if hasattr(cp, 'get_default_memory_pool'):
			mempool = cp.get_default_memory_pool()
			mempool.set_limit(size=5.6 * 1024**3)  # Alloue 5.6 Go de VRAM (70% des 8 Go disponibles)

# Configuration de Dask pour le calcul distribué
cluster = LocalCluster(
			n_workers=5,  # Utilisation de la moitié des cœurs physiques
			threads_per_worker=2,  # 2 threads par worker
			memory_limit='4GB'  # 20 Go total divisé par 5 workers
)
client = Client(cluster)

# Imports pour la visualisation
import matplotlib.pyplot as plt  # Pour la création de graphiques
import seaborn as sns  # Pour la création de visualisations statistiques

# Imports pour les statistiques
import scipy.stats as stats  # Pour les fonctions statistiques générales
from scipy.stats import randint, uniform, loguniform, norm  # Pour la génération de distributions aléatoires
from scipy.spatial.distance import cdist  # Pour le calcul de distances entre points

# Imports pour les modèles statistiques
import statsmodels.api as sm  # Pour les modèles statistiques
from statsmodels.stats.diagnostic import het_breuschpagan  # Pour le test de Breusch-Pagan
from statsmodels.stats.outliers_influence import variance_inflation_factor  # Pour le calcul du facteur d'inflation de la variance
from statsmodels.stats.stattools import durbin_watson  # Pour le test de Durbin-Watson


# Configuration de l'accélération Intel
from sklearnex import patch_sklearn  # Pour activer l'accélération Intel
patch_sklearn()  # Activer l'accélération Intel

# Imports scikit-learn
from sklearn.datasets import make_classification  # Pour la génération de données synthétiques

# Preprocessing et validation
from sklearn.model_selection import (
			GridSearchCV,  # Pour la recherche en grille d'hyperparamètres
			KFold,  # Pour la validation croisée k-fold
			RandomizedSearchCV,  # Pour la recherche aléatoire d'hyperparamètres
			StratifiedKFold,  # Pour la validation croisée stratifiée k-fold
			train_test_split,  # Pour la division des données en ensembles d'entraînement et de test
			cross_val_score,  # Pour l'évaluation de la performance d'un modèle par validation croisée
)
from sklearn.pipeline import Pipeline  # Pour la création de pipelines de machine learning
from sklearn.preprocessing import RobustScaler, StandardScaler  # Pour la mise à l'échelle des données
from sklearn.feature_selection import SelectKBest, f_regression

# Métriques et évaluation
from sklearn.metrics import (
			accuracy_score,  # Pour le calcul de la précision
			auc,  # Pour le calcul de l'aire sous la courbe ROC
			classification_report,  # Pour la génération d'un rapport de classification
			confusion_matrix,  # Pour la création d'une matrice de confusion
			f1_score,  # Pour le calcul du score F1
			mean_squared_error,  # Pour le calcul de l'erreur quadratique moyenne
			mean_absolute_error,  # Pour le calcul de l'erreur quadratique absolue
			precision_score,  # Pour le calcul de la précision
			recall_score,  # Pour le calcul du rappel
			roc_curve,  # Pour la création de la courbe ROC
			r2_score,  # Pour le calcul du score R²
			max_error,
			make_scorer,
			explained_variance_score,
			adjusted_rand_score, # Pour l'évaluation de la qualité du clustering
			silhouette_score, # Pour l'évaluation de la qualité du clustering 
)



# Modèles
from sklearn.cluster import KMeans  # Pour l'algorithme de clustering k-means
from sklearn.ensemble import RandomForestClassifier  # Pour le modèle de classification Random Forest
from sklearn.linear_model import LinearRegression,  Ridge, Lasso, LogisticRegression  # Pour les modèles de régression linéaire et de régression logistique
from sklearn.neighbors import KNeighborsClassifier  # Pour le modèle de classification k-plus proches voisins

# Utilitaires
import joblib  # Pour la sauvegarde et le chargement de modèles
import traceback


# Imports supplémentaires
from IPython.display import display  # Pour l'affichage d'objets dans un notebook Jupyter
from IPython.core.display import HTML  # Pour l'affichage de code HTML dans un notebook Jupyter
from math import pi  # Pour la constante pi
from sklearn.decomposition import PCA  # Pour l'analyse en composantes principales
import modified_xicor as xicorr # Pour le calcul de la corrélation de X

# Configuration des avertissements
import warnings
warnings.filterwarnings("ignore")  # Pour ignorer les avertissements

# Configuration du logging
logging.getLogger().handlers.clear()  # Nettoie les handlers existants
logging.basicConfig(
			level=logging.INFO,
			format="%(asctime)s - %(levelname)s - %(message)s",
			force=True  # Force la reconfiguration du logging
)

# Éviter la duplication des logs de sklearnex
sklearnex_logger = logging.getLogger("sklearnex")
sklearnex_logger.handlers.clear()  # Nettoie les handlers spécifiques à sklearnex
sklearnex_logger.propagate = False  # Empêche la propagation des logs vers le logger parent

# Configuration de Pandas
pd.set_option("display.max_columns", None)  # Pour afficher toutes les colonnes d'un DataFrame

# Configuration des visualisations
sns.set_style("whitegrid")  # Pour définir le style des graphiques
sns.set_palette("husl")  # Pour définir la palette de couleurs des graphiques
plt.rcParams["figure.figsize"] = [6, 4]  # Pour définir la taille des figures
plt.rcParams["font.size"] = 12  # Pour définir la taille de la police
plt.rcParams["axes.titlesize"] = 14  # Pour définir la taille des titres des axes
plt.rcParams["axes.labelsize"] = 12  # Pour définir la taille des étiquettes des axes
plt.rcParams["figure.dpi"] = 70  # Pour définir la résolution des figures
plt.rcParams["savefig.dpi"] = 300  # Pour définir la résolution des figures sauvegardées

# Configuration de la précision et de l'arrondi pour Decimal
getcontext().prec = 50  # Pour définir la précision des nombres décimaux
getcontext().rounding = ROUND_HALF_UP  # Pour définir le mode d'arrondi des nombres décimaux
Intel(R) Extension for Scikit-learn* enabled (https://github.com/intel/scikit-learn-intelex)
In [3]:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import RobustScaler, StandardScaler
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, accuracy_score, precision_score, recall_score, f1_score
import matplotlib.pyplot as plt
import joblib  # Pour sauvegarder le modèle
import altair as alt
In [4]:
# Définir le style 'dark_background'
plt.style.use('dark_background')

¶

1 - INTRODUCTION ET CHARGEMENT DES DONNEES
📋 Présentation du projet
🎯 Objectifs :

L'Organisation nationale de lutte contre le faux-monnayage (ONCFM) souhaite mettre en place un algorithme de détection automatique des faux billets en euros.


Contexte :
  • 🏦 L'ONCFM lutte contre la contrefaçon des billets en euros
  • 🔍 Des différences géométriques existent entre vrais et faux billets
  • 📊 Ces différences sont difficilement détectables à l'œil nu

Besoins :
  • 🤖 Automatiser la détection des faux billets
  • 📏 Utiliser les caractéristiques géométriques
  • 📈 Maximiser la précision de la détection
📊 Chargement et aperçu des données
  • 📥 Chargement du jeu de données `billets.csv` avec `pd.read_csv()`.
  • 🔍 Examen de la structure : dimensions (`.shape`), types (`.dtypes`), aperçu (`.head()`), statistiques descriptives (`.describe()`).
In [5]:
# Chargement des données
df = pd.read_csv("billets.csv", sep=";")
In [6]:
# Copie du DataFrame original
df_origin = df.copy()
In [7]:
df
Out[7]:
is_genuine diagonal height_left height_right margin_low margin_up length
0 True 171.81 104.86 104.95 4.52 2.89 112.83
1 True 171.46 103.36 103.66 3.77 2.99 113.09
2 True 172.69 104.48 103.50 4.40 2.94 113.16
3 True 171.36 103.91 103.94 3.62 3.01 113.51
4 True 171.73 104.28 103.46 4.04 3.48 112.54
... ... ... ... ... ... ... ...
1495 False 171.75 104.38 104.17 4.42 3.09 111.28
1496 False 172.19 104.63 104.44 5.27 3.37 110.97
1497 False 171.80 104.01 104.12 5.51 3.36 111.95
1498 False 172.06 104.28 104.06 5.17 3.46 112.25
1499 False 171.47 104.15 103.82 4.63 3.37 112.07

1500 rows × 7 columns

In [ ]:
 

¶

2 - ANALYSE EXPLORATOIRE

¶

2.1 - Analyse descriptive
In [7]:
display(pd.DataFrame([
	["=== APERÇU DES DONNÉES ==="],
	[f"Dimensions du dataset : {df.shape}"],
	["Types des variables :"],
]))

display(df.dtypes)

display("Aperçu des données :")
display(df.head())

display("Statistiques descriptives :")
display(df.describe())
0
0 === APERÇU DES DONNÉES ===
1 Dimensions du dataset : (1500, 7)
2 Types des variables :
is_genuine         bool
diagonal        float64
height_left     float64
height_right    float64
margin_low      float64
margin_up       float64
length          float64
dtype: object
'Aperçu des données :'
is_genuine diagonal height_left height_right margin_low margin_up length
0 True 171.81 104.86 104.95 4.52 2.89 112.83
1 True 171.46 103.36 103.66 3.77 2.99 113.09
2 True 172.69 104.48 103.50 4.40 2.94 113.16
3 True 171.36 103.91 103.94 3.62 3.01 113.51
4 True 171.73 104.28 103.46 4.04 3.48 112.54
'Statistiques descriptives :'
diagonal height_left height_right margin_low margin_up length
count 1500.000000 1500.000000 1500.000000 1463.000000 1500.000000 1500.00000
mean 171.958440 104.029533 103.920307 4.485967 3.151473 112.67850
std 0.305195 0.299462 0.325627 0.663813 0.231813 0.87273
min 171.040000 103.140000 102.820000 2.980000 2.270000 109.49000
25% 171.750000 103.820000 103.710000 4.015000 2.990000 112.03000
50% 171.960000 104.040000 103.920000 4.310000 3.140000 112.96000
75% 172.170000 104.230000 104.150000 4.870000 3.310000 113.34000
max 173.010000 104.880000 104.950000 6.900000 3.910000 114.44000
📊 Caractéristiques des données
Dataset :

  • 📈 1500 billets au total

  • ✅ 1000 vrais billets

  • ❌ 500 faux billets



Variables géométriques (en mm) :

  • 📏 length : longueur du billet

  • 📐 height_left : hauteur côté gauche

  • 📐 height_right : hauteur côté droit

  • ↕️ margin_up : marge supérieure

  • ↕️ margin_low : marge inférieure

  • ↗️ diagonal : diagonale du billet

¶

2.2 - Analyse univariée
In [8]:
def analyse_univariee(df):
	"""
	Effectue une analyse univariée descriptive pour chaque colonne d'un DataFrame.

	Args:
		df: DataFrame contenant les données.
	"""

	for col in df.columns:
		# Initialiser la sortie HTML pour la variable
		output = f"<h2>Analyse de la variable : {col}</h2>"

		# Afficher le nombre de valeurs manquantes
		nb_missing = df[col].isnull().sum()
		output += f"<p>Nombre de valeurs manquantes : {nb_missing}</p>"

		# Déterminer le type de la variable et effectuer les analyses appropriées
		if pd.api.types.is_numeric_dtype(df[col]):
			output += "<h3>Type de variable : Numérique</h3>"

			# Afficher les statistiques descriptives
			output += f"<p>{df[col].describe().to_frame().to_html()}</p>"  # Corrected line

			# Tests de normalité
			output += "<h4>Tests de normalité :</h4><p>"
			if len(df[col].dropna()) >= 3:  # Vérifier si la taille de l'échantillon est suffisante
				statistic, p_value = stats.shapiro(df[col].dropna())
				output += f"Test de Shapiro-Wilk : statistique={statistic:.3f}, p-value={p_value:.3f}<br>"
				if p_value < 0.05:
					output += "La distribution ne semble pas normale (Shapiro-Wilk).<br>"
				else:
					output += "La distribution semble normale (Shapiro-Wilk).<br>"

				statistic, p_value = stats.kstest(df[col].dropna(), 'norm')
				output += f"Test de Kolmogorov-Smirnov : statistique={statistic:.3f}, p-value={p_value:.3f}<br>"
				if p_value < 0.05:
					output += "La distribution ne semble pas normale (Kolmogorov-Smirnov).<br>"
				else:
					output += "La distribution semble normale (Kolmogorov-Smirnov)."
			else:
				output += "Tests de normalité non effectués : taille de l'échantillon insuffisante."
			output += "</p>"

			# Histogramme et boxplot sur la même figure
			fig, axes = plt.subplots(1, 2, figsize=(12, 4))
			sns.histplot(df[col], ax=axes[0])
			axes[0].set_title(f"Histogramme de {col}")
			sns.boxplot(y=df[col], ax=axes[1])
			axes[1].set_title(f"Boxplot de {col}")
			plt.show()

		elif pd.api.types.is_categorical_dtype(df[col]) or pd.api.types.is_object_dtype(df[col]):
			output += "<h3>Type de variable : Catégorielle</h3>"

			# Afficher les fréquences et les proportions
			output += "<h4>Fréquences :</h4>"
			output += f"<p>{df[col].value_counts().to_frame().to_html()}</p>"  # Corrected line
			output += "<h4>Proportions :</h4>"
			output += f"<p>{df[col].value_counts(normalize=True).to_frame().to_html()}</p>"  # Corrected line

			# Diagramme en barres
			plt.figure()
			df[col].value_counts().plot(kind='bar')
			plt.title(f"Diagramme en barres de {col}")
			plt.show()

		elif pd.api.types.is_datetime64_any_dtype(df[col]):
			output += "<h3>Type de variable : Date</h3>"

			# Afficher les statistiques descriptives pour les dates
			output += f"<p>{df[col].describe().to_html()}</p>"

			# Histogramme des dates
			plt.figure()
			df[col].hist()
			plt.title(f"Histogramme de {col}")
			plt.show()

		else:
			# Gérer les types non reconnus (booléens, etc.)
			output += f"<h3>Type de variable : Autre ({df[col].dtype})</h3>"
			output += f"<p>{df[col].describe().to_html()}</p>"

		# Afficher la sortie HTML pour la variable
		display(HTML(output))
In [9]:
analyse_univariee(df)
No description has been provided for this image

Analyse de la variable : is_genuine

Nombre de valeurs manquantes : 0

Type de variable : Numérique

is_genuine
count 1500
unique 2
top True
freq 1000

Tests de normalité :

Test de Shapiro-Wilk : statistique=0.595, p-value=0.000
La distribution ne semble pas normale (Shapiro-Wilk).
Test de Kolmogorov-Smirnov : statistique=0.508, p-value=0.000
La distribution ne semble pas normale (Kolmogorov-Smirnov).

No description has been provided for this image

Analyse de la variable : diagonal

Nombre de valeurs manquantes : 0

Type de variable : Numérique

diagonal
count 1500.000000
mean 171.958440
std 0.305195
min 171.040000
25% 171.750000
50% 171.960000
75% 172.170000
max 173.010000

Tests de normalité :

Test de Shapiro-Wilk : statistique=0.999, p-value=0.324
La distribution semble normale (Shapiro-Wilk).
Test de Kolmogorov-Smirnov : statistique=1.000, p-value=0.000
La distribution ne semble pas normale (Kolmogorov-Smirnov).

No description has been provided for this image

Analyse de la variable : height_left

Nombre de valeurs manquantes : 0

Type de variable : Numérique

height_left
count 1500.000000
mean 104.029533
std 0.299462
min 103.140000
25% 103.820000
50% 104.040000
75% 104.230000
max 104.880000

Tests de normalité :

Test de Shapiro-Wilk : statistique=0.998, p-value=0.051
La distribution semble normale (Shapiro-Wilk).
Test de Kolmogorov-Smirnov : statistique=1.000, p-value=0.000
La distribution ne semble pas normale (Kolmogorov-Smirnov).

No description has been provided for this image

Analyse de la variable : height_right

Nombre de valeurs manquantes : 0

Type de variable : Numérique

height_right
count 1500.000000
mean 103.920307
std 0.325627
min 102.820000
25% 103.710000
50% 103.920000
75% 104.150000
max 104.950000

Tests de normalité :

Test de Shapiro-Wilk : statistique=1.000, p-value=0.980
La distribution semble normale (Shapiro-Wilk).
Test de Kolmogorov-Smirnov : statistique=1.000, p-value=0.000
La distribution ne semble pas normale (Kolmogorov-Smirnov).

No description has been provided for this image

Analyse de la variable : margin_low

Nombre de valeurs manquantes : 37

Type de variable : Numérique

margin_low
count 1463.000000
mean 4.485967
std 0.663813
min 2.980000
25% 4.015000
50% 4.310000
75% 4.870000
max 6.900000

Tests de normalité :

Test de Shapiro-Wilk : statistique=0.938, p-value=0.000
La distribution ne semble pas normale (Shapiro-Wilk).
Test de Kolmogorov-Smirnov : statistique=0.999, p-value=0.000
La distribution ne semble pas normale (Kolmogorov-Smirnov).

No description has been provided for this image

Analyse de la variable : margin_up

Nombre de valeurs manquantes : 0

Type de variable : Numérique

margin_up
count 1500.000000
mean 3.151473
std 0.231813
min 2.270000
25% 2.990000
50% 3.140000
75% 3.310000
max 3.910000

Tests de normalité :

Test de Shapiro-Wilk : statistique=0.996, p-value=0.001
La distribution ne semble pas normale (Shapiro-Wilk).
Test de Kolmogorov-Smirnov : statistique=0.994, p-value=0.000
La distribution ne semble pas normale (Kolmogorov-Smirnov).

No description has been provided for this image

Analyse de la variable : length

Nombre de valeurs manquantes : 0

Type de variable : Numérique

length
count 1500.00000
mean 112.67850
std 0.87273
min 109.49000
25% 112.03000
50% 112.96000
75% 113.34000
max 114.44000

Tests de normalité :

Test de Shapiro-Wilk : statistique=0.918, p-value=0.000
La distribution ne semble pas normale (Shapiro-Wilk).
Test de Kolmogorov-Smirnov : statistique=1.000, p-value=0.000
La distribution ne semble pas normale (Kolmogorov-Smirnov).

¶

2.3 - Analyse bi-variée
In [10]:
# Fonction d'analyse bivariée
def analyse_bivariee(df):
    """
    Effectue une analyse bivariée pour chaque paire de colonnes d'un DataFrame.

    Args:
        df: DataFrame contenant les données.
    """
    import pandas as pd
    import numpy as np
    from scipy import stats
    import seaborn as sns
    import matplotlib.pyplot as plt
    from IPython.display import display, HTML
    import modified_xicor as xicorr

    for col1 in df.columns:
        for col2 in df.columns:
            if col1 != col2:
                # Initialiser la sortie HTML pour la paire de variables
                output = f"<h2>Analyse bivariée de {col1} et {col2}</h2>"

                # Afficher le nombre de valeurs manquantes
                nb_missing = df[[col1, col2]].isnull().any(axis=1).sum()
                output += f"<p>Nombre de valeurs manquantes : {nb_missing}</p>"

                # Déterminer les types de variables
                type_col1 = "Numérique" if pd.api.types.is_numeric_dtype(df[col1]) else "Catégorielle"
                type_col2 = "Numérique" if pd.api.types.is_numeric_dtype(df[col2]) else "Catégorielle"
                output += f"<p>Types de variables : {col1} ({type_col1}), {col2} ({type_col2})</p>"

                if type_col1 == "Numérique" and type_col2 == "Numérique":
                    # Supprimer les valeurs manquantes pour les deux colonnes simultanément
                    df_clean = df[[col1, col2]].dropna()

                    # Vérifier si la longueur est suffisante après avoir supprimé les valeurs manquantes
                    if len(df_clean) >= 2:
                        # Corrélations
                        output += "<h3>Corrélations :</h3><p>"

                        # Corrélation de Pearson
                        corr_pearson, p_value_pearson = stats.pearsonr(df_clean[col1], df_clean[col2])
                        output += f"Corrélation de Pearson : {corr_pearson:.3f}, p-value={p_value_pearson:.3f}<br>"

                        # Corrélation de Spearman
                        corr_spearman, p_value_spearman = stats.spearmanr(df_clean[col1], df_clean[col2])
                        output += f"Corrélation de Spearman : {corr_spearman:.3f}, p-value={p_value_spearman:.3f}<br>"

                        # Corrélation Xicorr
                        try:
                            x = np.array(df_clean[col1])
                            y = np.array(df_clean[col2])
                            result = xicorr.xicor(x, y)
                            # Utiliser la corrélation 'ordered'
                            xi = result['ordered']
                            # Différence entre ordered et unordered comme indicateur de significativité
                            diff = abs(result['ordered'] - result['unordered'])
                            # Si la différence est significative, on considère p < 0.05
                            p_value_xi = 0.001 if diff > 0.1 else 0.999
                            output += f"Corrélation Xicorr (ordered) : {xi:.3f}, différence ordered-unordered={diff:.3f}"
                        except Exception as e:
                            output += f"Calcul de Xicorr échoué. Erreur: {str(e)}"
                            p_value_xi = None
                        output += "</p>"

                        # Interprétation des corrélations
                        output += "<h4>Interprétation des corrélations :</h4><p>"
                        if p_value_pearson < 0.05:
                            output += f"Il existe une corrélation linéaire significative entre {col1} et {col2}.<br>"
                        else:
                            output += f"Il n'existe pas de corrélation linéaire significative entre {col1} et {col2}.<br>"
                        if p_value_spearman < 0.05:
                            output += f"Il existe une corrélation monotone significative entre {col1} et {col2}.<br>"
                        else:
                            output += f"Il n'existe pas de corrélation monotone significative entre {col1} et {col2}.<br>"
                        if p_value_xi is not None and p_value_xi < 0.05:
                            output += f"Il existe une dépendance non-monotone significative entre {col1} et {col2}."
                        else:
                            output += f"Il n'existe pas de dépendance non-monotone significative entre {col1} et {col2}."
                        output += "</p>"

                        # Nuage de points avec courbes de densité marginales
                        g = sns.jointplot(x=df_clean[col1], y=df_clean[col2], kind="kde")
                        g.fig.suptitle(f"Nuage de points et densités marginales entre {col1} et {col2}")
                        plt.show()

                        # Test de Levene pour l'égalité des variances
                        statistic, p_value = stats.levene(df_clean[col1], df_clean[col2])
                        output += f"<p>Test de Levene pour l'égalité des variances : statistique={statistic:.3f}, p-value={p_value:.3f}</p>"
                        if p_value < 0.05:
                            output += "<p>Les variances ne sont pas homogènes.</p>"
                        else:
                            output += "<p>Les variances sont homogènes.</p>"
                    else:
                        output += "<p>Pas assez de données pour calculer les corrélations après suppression des valeurs manquantes.</p>"

                elif type_col1 == "Catégorielle" and type_col2 == "Catégorielle":
                    # Tableau croisé
                    output += "<h3>Tableau croisé :</h3>"
                    output += f"<p>{pd.crosstab(df[col1], df[col2]).to_html()}</p>"

                    # Heatmap
                    plt.figure()
                    sns.heatmap(pd.crosstab(df[col1], df[col2]), annot=True, fmt='d', cmap="YlGnBu")
                    plt.title(f"Heatmap entre {col1} et {col2}")
                    plt.show()

                    # Test du chi-deux
                    chi2, p_value, dof, expected = stats.chi2_contingency(pd.crosstab(df[col1], df[col2]))
                    output += f"<p>Test du chi-deux : chi2={chi2:.3f}, p-value={p_value:.3f}, degrés de liberté={dof}</p>"
                    if p_value < 0.05:
                        output += f"<p>Il existe une association significative entre {col1} et {col2}.</p>"
                    else:
                        output += f"<p>Il n'existe pas d'association significative entre {col1} et {col2}.</p>"

                else:
                    # Une variable numérique et une catégorielle
                    num_col = col1 if type_col1 == "Numérique" else col2
                    cat_col = col2 if type_col1 == "Numérique" else col1

                    # ANOVA ou Kruskal-Wallis
                    groups = df[cat_col].unique()
                    samples = [df[num_col][df[cat_col] == g].dropna() for g in groups]
                    if all(stats.shapiro(s)[1] > 0.05 for s in samples if len(s) >= 3):
                        statistic, p_value = stats.f_oneway(*samples)
                        output += f"<p>ANOVA pour {num_col} en fonction de {cat_col} : statistique={statistic:.3f}, p-value={p_value:.3f}</p>"
                        if p_value < 0.05:
                            output += f"<p>Il existe une différence significative de {num_col} entre les groupes de {cat_col} (ANOVA).</p>"
                        else:
                            output += f"<p>Il n'existe pas de différence significative de {num_col} entre les groupes de {cat_col} (ANOVA).</p>"

                        # Test de Tukey HSD pour les comparaisons multiples
                        from statsmodels.stats.multicomp import pairwise_tukeyhsd
                        m_comp = pairwise_tukeyhsd(df[num_col], df[cat_col])
                        output += f"<p>{m_comp.summary().as_html()}</p>"

                    else:
                        statistic, p_value = stats.kruskal(*samples)
                        output += f"<p>Kruskal-Wallis pour {num_col} en fonction de {cat_col} : statistique={statistic:.3f}, p-value={p_value:.3f}</p>"
                        if p_value < 0.05:
                            output += f"<p>Il existe une différence significative de {num_col} entre les groupes de {cat_col} (Kruskal-Wallis).</p>"
                        else:
                            output += f"<p>Il n'existe pas de différence significative de {num_col} entre les groupes de {cat_col} (Kruskal-Wallis).</p>"

                        # Test de Dunn pour les comparaisons multiples
                        from scikit_posthocs import posthoc_dunn
                        output += f"<p>{posthoc_dunn(df, val_col=num_col, group_col=cat_col, p_adjust='bonferroni').to_html()}</p>"

                    # Boxplot
                    plt.figure()
                    sns.boxplot(x=df[cat_col], y=df[num_col])
                    plt.title(f"Boxplot de {num_col} en fonction de {cat_col}")
                    plt.show()

                # Afficher la sortie HTML pour la paire de variables
                display(HTML(output))
In [11]:
def analyse_bivariee(df):
    """
    Effectue une analyse bivariée pour chaque paire unique de colonnes d'un DataFrame.
    Optimise l'ordre des variables et évite les redondances.

    Args:
        df: DataFrame contenant les données.
    """
    import pandas as pd
    import numpy as np
    from scipy import stats
    import seaborn as sns
    import matplotlib.pyplot as plt
    from IPython.display import display, HTML
    import modified_xicor as xicorr

    # Créer un ensemble pour suivre les paires déjà analysées
    paires_analysees = set()

    for i, col1 in enumerate(df.columns):
        for col2 in df.columns[i+1:]:  # Commence à i+1 pour éviter les redondances
            # Créer une clé unique pour la paire (ordre indépendant)
            paire_key = tuple(sorted([col1, col2]))
            
            # Vérifier si cette paire a déjà été analysée
            if paire_key in paires_analysees:
                continue
                
            paires_analysees.add(paire_key)

            # Déterminer les types de variables
            type_col1 = "Numérique" if pd.api.types.is_numeric_dtype(df[col1]) else "Catégorielle"
            type_col2 = "Numérique" if pd.api.types.is_numeric_dtype(df[col2]) else "Catégorielle"
            
            # Déterminer si les variables sont booléennes
            is_bool1 = df[col1].dtype == bool or set(df[col1].unique()) <= {0, 1}
            is_bool2 = df[col2].dtype == bool or set(df[col2].unique()) <= {0, 1}

            # Réorganiser l'ordre pour mettre la variable numérique non-booléenne en premier
            if type_col1 == "Numérique" and type_col2 == "Numérique":
                if is_bool1 and not is_bool2:
                    col1, col2 = col2, col1
                    type_col1, type_col2 = type_col2, type_col1
                    is_bool1, is_bool2 = is_bool2, is_bool1

            # Initialiser la sortie HTML
            output = f"<h2>Analyse bivariée de {col1} et {col2}</h2>"
            
            # Afficher le nombre de valeurs manquantes
            nb_missing = df[[col1, col2]].isnull().any(axis=1).sum()
            output += f"<p>Nombre de valeurs manquantes : {nb_missing}</p>"
            
            # Afficher les types de variables
            output += f"<p>Types de variables : {col1} ({type_col1}')"
            if is_bool1:
                output += " [Booléenne]"
            output += f", {col2} ({type_col2})"
            if is_bool2:
                output += " [Booléenne]"
            output += "</p>"

            # Le reste du code reste similaire, mais utilise col1 et col2 dans leur nouvel ordre
            # [Code existant pour l'analyse des différents types de variables]
            
            if type_col1 == "Numérique" and type_col2 == "Numérique":
                # Supprimer les valeurs manquantes
                df_clean = df[[col1, col2]].dropna()

                if len(df_clean) >= 2:
                    # Corrélations
                    output += "<h3>Corrélations :</h3><p>"

                    # Corrélation de Pearson
                    corr_pearson, p_value_pearson = stats.pearsonr(df_clean[col1], df_clean[col2])
                    output += f"Corrélation de Pearson : {corr_pearson:.3f}, p-value={p_value_pearson:.3f}<br>"

                    # Corrélation de Spearman
                    corr_spearman, p_value_spearman = stats.spearmanr(df_clean[col1], df_clean[col2])
                    output += f"Corrélation de Spearman : {corr_spearman:.3f}, p-value={p_value_spearman:.3f}<br>"

                    # Corrélation Xicorr
                    try:
                        x = np.array(df_clean[col1])
                        y = np.array(df_clean[col2])
                        result = xicorr.xicor(x, y)
                        xi = result['ordered']
                        diff = abs(result['ordered'] - result['unordered'])
                        p_value_xi = 0.001 if diff > 0.1 else 0.999
                        output += f"Corrélation Xicorr (ordered) : {xi:.3f}, différence ordered-unordered={diff:.3f}"
                    except Exception as e:
                        output += f"Calcul de Xicorr échoué. Erreur: {str(e)}"
                        p_value_xi = None
                    output += "</p>"

                    # Interprétation des corrélations
                    output += "<h4>Interprétation des corrélations :</h4><p>"
                    if p_value_pearson < 0.05:
                        output += f"Il existe une corrélation linéaire significative entre {col1} et {col2}."
                        if corr_pearson > 0:
                            output += " La relation est positive.<br>"
                        else:
                            output += " La relation est négative.<br>"
                    else:
                        output += f"Il n'existe pas de corrélation linéaire significative entre {col1} et {col2}.<br>"
                    
                    if p_value_spearman < 0.05:
                        output += f"Il existe une corrélation monotone significative entre {col1} et {col2}.<br>"
                    else:
                        output += f"Il n'existe pas de corrélation monotone significative entre {col1} et {col2}.<br>"
                    
                    if p_value_xi is not None and p_value_xi < 0.05:
                        output += f"Il existe une dépendance non-monotone significative entre {col1} et {col2}."
                    else:
                        output += f"Il n'existe pas de dépendance non-monotone significative entre {col1} et {col2}."
                    output += "</p>"

                    # Nuage de points avec courbes de densité marginales
                    g = sns.jointplot(x=df_clean[col1], y=df_clean[col2], kind="kde")
                    g.fig.suptitle(f"Nuage de points et densités marginales entre {col1} et {col2}")
                    plt.show()

                    # Test de Levene seulement si les deux variables ne sont pas booléennes
                    if not (is_bool1 or is_bool2):
                        statistic, p_value = stats.levene(df_clean[col1], df_clean[col2])
                        output += f"<p>Test de Levene pour l'égalité des variances : statistique={statistic:.3f}, p-value={p_value:.3f}</p>"
                        if p_value < 0.05:
                            output += "<p>Les variances ne sont pas homogènes.</p>"
                        else:
                            output += "<p>Les variances sont homogènes.</p>"

            elif type_col1 == "Catégorielle" and type_col2 == "Catégorielle":
                # Tableau croisé
                output += "<h3>Tableau croisé :</h3>"
                output += f"<p>{pd.crosstab(df[col1], df[col2]).to_html()}</p>"

                # Heatmap
                plt.figure()
                sns.heatmap(pd.crosstab(df[col1], df[col2]), annot=True, fmt='d', cmap="YlGnBu")
                plt.title(f"Heatmap entre {col1} et {col2}")
                plt.show()

                # Test du chi-deux
                chi2, p_value, dof, expected = stats.chi2_contingency(pd.crosstab(df[col1], df[col2]))
                output += f"<p>Test du chi-deux : chi2={chi2:.3f}, p-value={p_value:.3f}, degrés de liberté={dof}</p>"
                if p_value < 0.05:
                    output += f"<p>Il existe une association significative entre {col1} et {col2}.</p>"
                else:
                    output += f"<p>Il n'existe pas d'association significative entre {col1} et {col2}.</p>"

            else:
                # Une variable numérique et une catégorielle
                num_col = col1 if type_col1 == "Numérique" else col2
                cat_col = col2 if type_col1 == "Numérique" else col1

                # Boxplot
                plt.figure()
                sns.boxplot(x=df[cat_col], y=df[num_col])
                plt.title(f"Boxplot de {num_col} en fonction de {cat_col}")
                plt.show()

                # ANOVA ou Kruskal-Wallis
                groups = df[cat_col].unique()
                samples = [df[num_col][df[cat_col] == g].dropna() for g in groups]
                
                # Test de normalité seulement si assez d'échantillons
                if all(len(s) >= 3 for s in samples):
                    if all(stats.shapiro(s)[1] > 0.05 for s in samples):
                        # ANOVA
                        statistic, p_value = stats.f_oneway(*samples)
                        output += f"<p>ANOVA pour {num_col} en fonction de {cat_col} : statistique={statistic:.3f}, p-value={p_value:.3f}</p>"
                        test_type = "ANOVA"
                    else:
                        # Kruskal-Wallis
                        statistic, p_value = stats.kruskal(*samples)
                        output += f"<p>Kruskal-Wallis pour {num_col} en fonction de {cat_col} : statistique={statistic:.3f}, p-value={p_value:.3f}</p>"
                        test_type = "Kruskal-Wallis"

                    if p_value < 0.05:
                        output += f"<p>Il existe une différence significative de {num_col} entre les groupes de {cat_col} ({test_type}).</p>"
                    else:
                        output += f"<p>Il n'existe pas de différence significative de {num_col} entre les groupes de {cat_col} ({test_type}).</p>"

            # Afficher la sortie HTML
            display(HTML(output))
In [12]:
analyse_bivariee(df)
No description has been provided for this image

Analyse bivariée de diagonal et is_genuine

Nombre de valeurs manquantes : 0

Types de variables : diagonal (Numérique'), is_genuine (Numérique) [Booléenne]

Corrélations :

Corrélation de Pearson : 0.133, p-value=0.000
Corrélation de Spearman : 0.129, p-value=0.000
Corrélation Xicorr (ordered) : 0.669, différence ordered-unordered=0.219

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre diagonal et is_genuine. La relation est positive.
Il existe une corrélation monotone significative entre diagonal et is_genuine.
Il existe une dépendance non-monotone significative entre diagonal et is_genuine.

No description has been provided for this image

Analyse bivariée de diagonal et height_left

Nombre de valeurs manquantes : 0

Types de variables : diagonal (Numérique'), height_left (Numérique)

Corrélations :

Corrélation de Pearson : 0.019, p-value=0.451
Corrélation de Spearman : 0.020, p-value=0.438
Corrélation Xicorr (ordered) : 0.580, différence ordered-unordered=0.022

Interprétation des corrélations :

Il n'existe pas de corrélation linéaire significative entre diagonal et height_left.
Il n'existe pas de corrélation monotone significative entre diagonal et height_left.
Il n'existe pas de dépendance non-monotone significative entre diagonal et height_left.

Test de Levene pour l'égalité des variances : statistique=0.381, p-value=0.537

Les variances sont homogènes.

No description has been provided for this image

Analyse bivariée de diagonal et height_right

Nombre de valeurs manquantes : 0

Types de variables : diagonal (Numérique'), height_right (Numérique)

Corrélations :

Corrélation de Pearson : -0.024, p-value=0.343
Corrélation de Spearman : -0.030, p-value=0.250
Corrélation Xicorr (ordered) : 0.587, différence ordered-unordered=0.002

Interprétation des corrélations :

Il n'existe pas de corrélation linéaire significative entre diagonal et height_right.
Il n'existe pas de corrélation monotone significative entre diagonal et height_right.
Il n'existe pas de dépendance non-monotone significative entre diagonal et height_right.

Test de Levene pour l'égalité des variances : statistique=5.498, p-value=0.019

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de diagonal et margin_low

Nombre de valeurs manquantes : 37

Types de variables : diagonal (Numérique'), margin_low (Numérique)

Corrélations :

Corrélation de Pearson : -0.112, p-value=0.000
Corrélation de Spearman : -0.104, p-value=0.000
Corrélation Xicorr (ordered) : 0.578, différence ordered-unordered=0.034

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre diagonal et margin_low. La relation est négative.
Il existe une corrélation monotone significative entre diagonal et margin_low.
Il n'existe pas de dépendance non-monotone significative entre diagonal et margin_low.

Test de Levene pour l'égalité des variances : statistique=419.613, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de diagonal et margin_up

Nombre de valeurs manquantes : 0

Types de variables : diagonal (Numérique'), margin_up (Numérique)

Corrélations :

Corrélation de Pearson : -0.056, p-value=0.031
Corrélation de Spearman : -0.051, p-value=0.048
Corrélation Xicorr (ordered) : 0.581, différence ordered-unordered=0.002

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre diagonal et margin_up. La relation est négative.
Il existe une corrélation monotone significative entre diagonal et margin_up.
Il n'existe pas de dépendance non-monotone significative entre diagonal et margin_up.

Test de Levene pour l'égalité des variances : statistique=91.066, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de diagonal et length

Nombre de valeurs manquantes : 0

Types de variables : diagonal (Numérique'), length (Numérique)

Corrélations :

Corrélation de Pearson : 0.098, p-value=0.000
Corrélation de Spearman : 0.096, p-value=0.000
Corrélation Xicorr (ordered) : 0.579, différence ordered-unordered=0.043

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre diagonal et length. La relation est positive.
Il existe une corrélation monotone significative entre diagonal et length.
Il n'existe pas de dépendance non-monotone significative entre diagonal et length.

Test de Levene pour l'égalité des variances : statistique=695.064, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de height_left et height_right

Nombre de valeurs manquantes : 0

Types de variables : height_left (Numérique'), height_right (Numérique)

Corrélations :

Corrélation de Pearson : 0.242, p-value=0.000
Corrélation de Spearman : 0.254, p-value=0.000
Corrélation Xicorr (ordered) : 0.589, différence ordered-unordered=0.092

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre height_left et height_right. La relation est positive.
Il existe une corrélation monotone significative entre height_left et height_right.
Il n'existe pas de dépendance non-monotone significative entre height_left et height_right.

Test de Levene pour l'égalité des variances : statistique=8.787, p-value=0.003

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de height_left et margin_low

Nombre de valeurs manquantes : 37

Types de variables : height_left (Numérique'), margin_low (Numérique)

Corrélations :

Corrélation de Pearson : 0.303, p-value=0.000
Corrélation de Spearman : 0.297, p-value=0.000
Corrélation Xicorr (ordered) : 0.605, différence ordered-unordered=0.098

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre height_left et margin_low. La relation est positive.
Il existe une corrélation monotone significative entre height_left et margin_low.
Il n'existe pas de dépendance non-monotone significative entre height_left et margin_low.

Test de Levene pour l'égalité des variances : statistique=437.032, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de height_left et margin_up

Nombre de valeurs manquantes : 0

Types de variables : height_left (Numérique'), margin_up (Numérique)

Corrélations :

Corrélation de Pearson : 0.247, p-value=0.000
Corrélation de Spearman : 0.260, p-value=0.000
Corrélation Xicorr (ordered) : 0.606, différence ordered-unordered=0.093

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre height_left et margin_up. La relation est positive.
Il existe une corrélation monotone significative entre height_left et margin_up.
Il n'existe pas de dépendance non-monotone significative entre height_left et margin_up.

Test de Levene pour l'égalité des variances : statistique=80.437, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de height_left et length

Nombre de valeurs manquantes : 0

Types de variables : height_left (Numérique'), length (Numérique)

Corrélations :

Corrélation de Pearson : -0.321, p-value=0.000
Corrélation de Spearman : -0.309, p-value=0.000
Corrélation Xicorr (ordered) : 0.613, différence ordered-unordered=0.158

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre height_left et length. La relation est négative.
Il existe une corrélation monotone significative entre height_left et length.
Il existe une dépendance non-monotone significative entre height_left et length.

Test de Levene pour l'égalité des variances : statistique=710.541, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de height_right et margin_low

Nombre de valeurs manquantes : 37

Types de variables : height_right (Numérique'), margin_low (Numérique)

Corrélations :

Corrélation de Pearson : 0.391, p-value=0.000
Corrélation de Spearman : 0.397, p-value=0.000
Corrélation Xicorr (ordered) : 0.604, différence ordered-unordered=0.118

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre height_right et margin_low. La relation est positive.
Il existe une corrélation monotone significative entre height_right et margin_low.
Il existe une dépendance non-monotone significative entre height_right et margin_low.

Test de Levene pour l'égalité des variances : statistique=367.617, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de height_right et margin_up

Nombre de valeurs manquantes : 0

Types de variables : height_right (Numérique'), margin_up (Numérique)

Corrélations :

Corrélation de Pearson : 0.307, p-value=0.000
Corrélation de Spearman : 0.303, p-value=0.000
Corrélation Xicorr (ordered) : 0.588, différence ordered-unordered=0.107

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre height_right et margin_up. La relation est positive.
Il existe une corrélation monotone significative entre height_right et margin_up.
Il existe une dépendance non-monotone significative entre height_right et margin_up.

Test de Levene pour l'égalité des variances : statistique=138.667, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de height_right et length

Nombre de valeurs manquantes : 0

Types de variables : height_right (Numérique'), length (Numérique)

Corrélations :

Corrélation de Pearson : -0.402, p-value=0.000
Corrélation de Spearman : -0.371, p-value=0.000
Corrélation Xicorr (ordered) : 0.609, différence ordered-unordered=0.213

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre height_right et length. La relation est négative.
Il existe une corrélation monotone significative entre height_right et length.
Il existe une dépendance non-monotone significative entre height_right et length.

Test de Levene pour l'égalité des variances : statistique=637.038, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de margin_low et margin_up

Nombre de valeurs manquantes : 37

Types de variables : margin_low (Numérique'), margin_up (Numérique)

Corrélations :

Corrélation de Pearson : 0.432, p-value=0.000
Corrélation de Spearman : 0.421, p-value=0.000
Corrélation Xicorr (ordered) : 0.473, différence ordered-unordered=0.167

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre margin_low et margin_up. La relation est positive.
Il existe une corrélation monotone significative entre margin_low et margin_up.
Il existe une dépendance non-monotone significative entre margin_low et margin_up.

Test de Levene pour l'égalité des variances : statistique=658.711, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de margin_low et length

Nombre de valeurs manquantes : 37

Types de variables : margin_low (Numérique'), length (Numérique)

Corrélations :

Corrélation de Pearson : -0.667, p-value=0.000
Corrélation de Spearman : -0.588, p-value=0.000
Corrélation Xicorr (ordered) : 0.564, différence ordered-unordered=0.586

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre margin_low et length. La relation est négative.
Il existe une corrélation monotone significative entre margin_low et length.
Il existe une dépendance non-monotone significative entre margin_low et length.

Test de Levene pour l'égalité des variances : statistique=73.831, p-value=0.000

Les variances ne sont pas homogènes.

No description has been provided for this image

Analyse bivariée de margin_up et length

Nombre de valeurs manquantes : 0

Types de variables : margin_up (Numérique'), length (Numérique)

Corrélations :

Corrélation de Pearson : -0.521, p-value=0.000
Corrélation de Spearman : -0.480, p-value=0.000
Corrélation Xicorr (ordered) : 0.702, différence ordered-unordered=0.253

Interprétation des corrélations :

Il existe une corrélation linéaire significative entre margin_up et length. La relation est négative.
Il existe une corrélation monotone significative entre margin_up et length.
Il existe une dépendance non-monotone significative entre margin_up et length.

Test de Levene pour l'égalité des variances : statistique=919.517, p-value=0.000

Les variances ne sont pas homogènes.

¶

2.4 - Matrice de corrélation heatmap
🔗 Relations entre variables

  • 🧮 Matrice de corrélation : `sns.heatmap(df.corr(), annot=True)` pour visualiser les corrélations entre toutes les variables numériques.

In [13]:
# Création de la figure
plt.figure(figsize=(12, 10))

# Créer un masque pour le triangle inférieur
mask = np.triu(np.ones_like(df.corr()))

# Créer la heatmap avec le masque
sns.heatmap(df.corr(), 
			mask=mask,
			annot=True,
			cmap='coolwarm',
			center=0,
			vmin=-1,
			vmax=1,
			square=True,
			fmt='.2f')
plt.title('Matrice de corrélation entre les variables')
plt.tight_layout()
plt.show()

# Afficher les paires de variables fortement corrélées
display("\nPaires de variables fortement corrélées (|corr| > 0.6) :")

corr_matrix = df.corr()  # Garder les valeurs originales (pas abs())
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
strong_corrs = []

# Parcourir toutes les paires possibles
for i in range(len(upper.index)):
	for j in range(i + 1, len(upper.columns)):
		corr = upper.iloc[i, j]
		if corr is not None and abs(corr) > 0.6:  # Vérifier la valeur absolue
			strong_corrs.append((
				upper.index[i],
				upper.columns[j],
				corr  # Garder la valeur réelle de la corrélation
			))

# Trier par valeur absolue de corrélation
strong_corrs.sort(key=lambda x: abs(x[2]), reverse=True)

# Afficher les résultats
if strong_corrs:
	for var1, var2, corr in strong_corrs:
		display(f"{var1} - {var2}: {corr:.3f}")
else:
	display("Aucune paire de variables n'a de corrélation supérieure à 0.6 en valeur absolue")
No description has been provided for this image
'\nPaires de variables fortement corrélées (|corr| > 0.6) :'
'is_genuine - length: 0.849'
'is_genuine - margin_low: -0.783'
'margin_low - length: -0.667'
'is_genuine - margin_up: -0.606'

¶

2.5 - Comparaison des groupes de vrais et faux billets
📊 Analyse statistique comparative des billets
  • 📏 Variables analysées : ['diagonal', 'height_left', 'height_right', 'margin_up', 'margin_low', 'length']

○ 📊 ÉTAPE 1 - Visualisation : • Représentation graphique :

  • Histogrammes avec courbes de densité (sns.histplot(kde=True))
  • Boxplots pour détecter les valeurs aberrantes (sns.boxplot())

○ 🔍 ÉTAPE 2 - Test de Shapiro-Wilk :

Objectif : Vérifie la normalité des distributions

  • Hypothèse : H0 : La distribution suit une loi normale
  • Décision : - Si p-value > 0.05 : distribution normale → Passer à l'étape 3 - Si p-value ≤ 0.05 : distribution non normale → Aller directement au Mann-Whitney U (étape 4)

○ ⚖️ ÉTAPE 3 - Test de Levene :

Objectif : Vérifie l'égalité des variances entre les deux groupes

  • Hypothèse : H0 : Les variances sont égales
  • Décision : - Si p-value > 0.05 : variances égales → T-test classique - Si p-value ≤ 0.05 : variances inégales → Test de Welch

○ 📈 ÉTAPE 4 - Tests statistiques comparatifs :

  • T-test classique : si normalité + variances égales
  • Test de Welch : si normalité + variances inégales
  • Mann-Whitney U : si non normalité

○ 📉 Interprétation des p-values : • Hypothèse : H0 : Pas de différence entre les groupes

  • Décision : - p-value < 0.05 : différence significative (5% de risque de se tromper) - p-value ≥ 0.05 : pas de différence significative

In [14]:
# - Différence en pourcentage la sommes de chaque variables entre vrais et faux billets

# Charger un fichier CSV en utilisant Dask
df_dask = dd.read_csv("billets.csv", sep=";")

# Supprime les valeurs manquantes de margin low et sépare les 2 groupes
result = df_dask[df_dask['margin_low'] > 0].groupby('is_genuine').sum().compute()

# Créer un nouveau DataFrame result2 avec la ligne 'True' divisée par 2 
result2 = result.copy()
result2.loc[True] = result2.loc[True] / 2

# Calculer le pourcentage de différence entre les 2 groupes
result2.loc['diff en %'] = round(((result.loc[False] - result2.loc[True]) / result.loc[False]) * 100, 2)

# Trier les colonnes en fonction de 'diff en %'
result2 = result2.loc[:, result2.loc['diff en %'].sort_values().index]

display(result)
display(result2)

# Créer un graphique pour la ligne 'diff en %'
plt.figure(figsize=(10, 6))
sns.barplot(x=result2.columns, y=result2.loc['diff en %'], palette='viridis')
plt.title("Différence en pourcentage la sommes de chaque variables entre vrais et faux billets")
plt.ylabel('Différence en %')
plt.xlabel('')
plt.xticks(rotation=45)
plt.ylim(min(result2.loc['diff en %']) - 0.5, max(result2.loc['diff en %']) + 0.5)  # Ajuster l'échelle de l'axe y
plt.tight_layout()
plt.show()
diagonal height_left height_right margin_low margin_up length
is_genuine
False 84575.49 51260.76 51238.49 2566.24 1648.94 54923.00
True 167000.81 100937.08 100798.63 3996.73 2964.02 109920.17
length diagonal height_left height_right margin_up margin_low
is_genuine
False 54923.000 84575.490 51260.76 51238.490 1648.94 2566.240
True 54960.085 83500.405 50468.54 50399.315 1482.01 1998.365
diff en % -0.070 1.270 1.55 1.640 10.12 22.130
No description has been provided for this image
In [15]:
def compare_distributions(df):
	"""
	Compare les distributions de chaque variable numérique entre les vrais et les faux billets.

	Args:
	  df: DataFrame contenant les données.
	"""
	
	variables = ['diagonal', 'height_left', 'height_right', 'margin_up', 'margin_low', 'length']

	for var in variables:
		# Crée une nouvelle figure pour chaque variable
		plt.figure(figsize=(12, 4))

		# Affiche l'histogramme de la variable pour chaque groupe (vrais et faux billets)
		plt.subplot(1, 2, 1)
		sns.histplot(df[df['is_genuine'] == True][var], kde=True, color='blue', label='Vrais')
		sns.histplot(df[df['is_genuine'] == False][var], kde=True, color='red', label='Faux')
		plt.title(f'Distribution de {var} par type de billet')
		plt.xlabel(var)
		plt.ylabel('Fréquence')
		plt.legend()

		# Affiche le boxplot de la variable pour chaque groupe (vrais et faux billets)
		plt.subplot(1, 2, 2)
		sns.boxplot(x='is_genuine', y=var, data=df)
		plt.title(f'Boxplot de {var} par type de billet')
		plt.xlabel('Type de billet')
		plt.ylabel('Valeur (mm)')

		plt.tight_layout()
		plt.show()  # Affiche les graphiques ici

		# Sépare les données en deux groupes (vrais et faux billets) et supprime les valeurs manquantes
		genuine = df[df['is_genuine'] == True][var].dropna()
		fake = df[df['is_genuine'] == False][var].dropna()

		# Affichage des statistiques descriptives pour chaque groupe
		display(f"Stats des données {var}:")
		display(df.groupby('is_genuine')[var].describe())

		# Vérifie si la taille de chaque groupe est suffisante pour effectuer les tests statistiques
		if len(genuine) > 3 and len(fake) > 3:
			# Test de Shapiro-Wilk pour vérifier la normalité des distributions
			if stats.shapiro(genuine)[1] > 0.05 and stats.shapiro(fake)[1] > 0.05:
				# Test de Levene pour vérifier l'égalité des variances
				if stats.levene(genuine, fake)[1] > 0.05:
					# T-test classique si les variances sont égales
					test_stat, p_value = stats.ttest_ind(genuine, fake, equal_var=True)
					test_name = "t-test classique"
				else:
					# Welch t-test si les variances sont inégales
					test_stat, p_value = stats.ttest_ind(genuine, fake, equal_var=False)
					test_name = "Welch t-test"
			else:
				# Mann-Whitney U test si les distributions ne sont pas normales
				test_stat, p_value = stats.mannwhitneyu(genuine, fake)
				test_name = "Mann-Whitney U"

			display(f"Test statistique ({test_name}) pour {var}: p-value = {p_value:e}")
			display(f"- Différence {'significative' if p_value < 0.05 else 'non significative'} (seuil 5%)")
		else:
			display(f"Test statistique non effectué pour {var} : taille d'échantillon insuffisante.")
In [16]:
compare_distributions(df)
No description has been provided for this image
'Stats des données diagonal:'
count mean std min 25% 50% 75% max
is_genuine
False 500.0 171.90116 0.306861 171.04 171.69 171.91 172.0925 173.01
True 1000.0 171.98708 0.300441 171.04 171.79 171.99 172.2000 172.92
'Test statistique (t-test classique) pour diagonal: p-value = 2.466867e-07'
'- Différence significative (seuil 5%)'
No description has been provided for this image
'Stats des données height_left:'
count mean std min 25% 50% 75% max
is_genuine
False 500.0 104.19034 0.223758 103.51 104.04 104.18 104.3325 104.88
True 1000.0 103.94913 0.300231 103.14 103.74 103.95 104.1400 104.86
'Test statistique (Mann-Whitney U) pour height_left: p-value = 4.883939e-53'
'- Différence significative (seuil 5%)'
No description has been provided for this image
'Stats des données height_right:'
count mean std min 25% 50% 75% max
is_genuine
False 500.0 104.14362 0.270878 103.43 103.95 104.16 104.32 104.95
True 1000.0 103.80865 0.291570 102.82 103.61 103.81 104.00 104.95
'Test statistique (t-test classique) pour height_right: p-value = 2.270856e-89'
'- Différence significative (seuil 5%)'
No description has been provided for this image
'Stats des données margin_up:'
count mean std min 25% 50% 75% max
is_genuine
False 500.0 3.35016 0.180498 2.92 3.22 3.35 3.4725 3.91
True 1000.0 3.05213 0.186340 2.27 2.93 3.05 3.1800 3.74
'Test statistique (t-test classique) pour margin_up: p-value = 3.144530e-151'
'- Différence significative (seuil 5%)'
No description has been provided for this image
'Stats des données margin_low:'
count mean std min 25% 50% 75% max
is_genuine
False 492.0 5.215935 0.553531 3.82 4.840 5.19 5.5925 6.90
True 971.0 4.116097 0.319124 2.98 3.905 4.11 4.3400 5.04
'Test statistique (Welch t-test) pour margin_low: p-value = 1.433175e-182'
'- Différence significative (seuil 5%)'
No description has been provided for this image
'Stats des données length:'
count mean std min 25% 50% 75% max
is_genuine
False 500.0 111.63064 0.615543 109.49 111.20 111.630 112.03 113.85
True 1000.0 113.20243 0.359552 111.76 112.95 113.205 113.46 114.44
'Test statistique (Welch t-test) pour length: p-value = 1.469955e-241'
'- Différence significative (seuil 5%)'
📊 Conclusion


  • Les tests statistiques révèlent des différences significatives (p-value < 0.05) pour toutes les variables analysées


  • Analyse détaillée :

    • Les tests confirment que les distributions diffèrent entre vrais et faux billets

    • La visualisation montre des caractéristiques distinctes entre les deux groupes

    • Les boxplots révèlent des plages de valeurs différentes selon l'authenticité

  • Implications pratiques :

    • Ces variables permettent de discriminer efficacement les vrais des faux billets

    • La présence de différences significatives justifie leurs utilisations comme critères d'authentification

  • Point d'attention :

    • Malgré les différences significatives, les distributions présentent quelques zones de chevauchement

    • Une approche multi-critères est recommandée pour une authentification robuste

¶

2.6 - Distribution des valeurs manquantes au sein des 2 groupes
In [17]:
# Vérifier la répartition des valeurs manquantes dans 'margin_low' pour chaque groupe de 'is_genuine'
missing_genuine = df[df['is_genuine'] == True]['margin_low'].isnull().sum()
total_genuine = len(df[df['is_genuine'] == True])
percent_missing_genuine = (missing_genuine / total_genuine) * 100

missing_fake = df[df['is_genuine'] == False]['margin_low'].isnull().sum()
total_fake = len(df[df['is_genuine'] == False])
percent_missing_fake = (missing_fake / total_fake) * 100


display(f"Nombre de valeurs manquantes dans 'margin_low' pour les vrais billets: {missing_genuine} ({percent_missing_genuine:.2f}%)")
display(f"Nombre de valeurs manquantes dans 'margin_low' pour les faux billets: {missing_fake} ({percent_missing_fake:.2f}%)")


# Visualisation
plt.figure(figsize=(8, 6))
sns.countplot(x='is_genuine', hue='margin_low', data=df.assign(margin_low=df['margin_low'].isnull()))

# Modification de la légende
plt.legend(title='Valeurs manquantes', labels=['Présentes', 'Manquantes'])

plt.title('Répartition des valeurs manquantes margin_low')
plt.xlabel('is_genuine')
plt.ylabel('Nombre de billets')
plt.xticks([0, 1], ['Faux', 'Vrai'])
plt.show()
"Nombre de valeurs manquantes dans 'margin_low' pour les vrais billets: 29 (2.90%)"
"Nombre de valeurs manquantes dans 'margin_low' pour les faux billets: 8 (1.60%)"
No description has been provided for this image

¶

2.7 - Analyse des valeurs aberrantes à l'aide du Z-score
In [18]:
# Affichage du titre de l'analyse
display(pd.DataFrame({'': ['=== ANALYSE DES VALEURS ABERRANTES (DataFrame original) ===']}).style.hide(axis='index'))

try:
	# Extraction des noms de colonnes (features)
	features = df.columns.tolist()

	# Initialisation du dictionnaire pour stocker les résultats par variable
	outliers_by_var = {}

	# Affichage du titre de l'analyse par variable
	display(pd.DataFrame({'': ['=== ANALYSE PAR VARIABLE ===']}).style.hide(axis='index'))

	# Boucle sur chaque variable (feature)
	for col in features:
		# Exclure la variable cible 'is_genuine' de l'analyse des valeurs aberrantes
		if col != 'is_genuine':
			# Calcul des z-scores pour chaque observation de la variable
			z_scores = np.abs(stats.zscore(df[col]))

			# Identification des valeurs aberrantes (z-score > 3)
			outliers_mask = z_scores > 3

			# Identification des valeurs aberrantes pour chaque type de billet
			auth_outliers = outliers_mask & (df['is_genuine'] == 1)  # Vrais positifs
			fake_outliers = outliers_mask & (df['is_genuine'] == 0)  # Faux positifs

			# Stockage du nombre total de valeurs aberrantes et leur répartition par type de billet
			outliers_by_var[col] = {
				'total': outliers_mask.sum(),
				'authentiques': auth_outliers.sum(),
				'contrefaits': fake_outliers.sum()
			}

			# Calcul des statistiques descriptives pour la variable (sans les valeurs aberrantes)
			var_stats = pd.DataFrame({
				'Métrique': [
					'Total valeurs aberrantes',
					'Dans billets authentiques',
					'Dans billets contrefaits',
					'Minimum (sans aberrants)',
					'Maximum (sans aberrants)',
					'Moyenne (sans aberrants)',
					'Écart-type (sans aberrants)'
				],
				'Valeur': [
					f"{outliers_mask.sum()} ({(outliers_mask.sum()/len(df)*100):.2f}%)",
					f"{auth_outliers.sum()} ({(auth_outliers.sum()/(df['is_genuine'] == 1).sum()*100):.2f}%)",
					f"{fake_outliers.sum()} ({(fake_outliers.sum()/(df['is_genuine'] == 0).sum()*100):.2f}%)",
					f"{df[~outliers_mask][col].min():.3f}",
					f"{df[~outliers_mask][col].max():.3f}",
					f"{df[~outliers_mask][col].mean():.3f}",
					f"{df[~outliers_mask][col].std():.3f}"
				]
			})

			# Affichage du nom de la variable en cours d'analyse
			display(pd.DataFrame({'': [f'=== VARIABLE : {col} ===']}).style.hide(axis='index'))
			# Affichage des statistiques descriptives
			display(var_stats.style.hide(axis='index'))

			# Affichage des 5 valeurs les plus aberrantes si des valeurs aberrantes sont détectées
			if outliers_mask.sum() > 0:
				display(pd.DataFrame({'': [f'Top 5 des valeurs aberrantes pour {col}']}).style.hide(axis='index'))
				extreme_vals = df[outliers_mask].copy()  # Copie des valeurs aberrantes
				extreme_vals['Z-score'] = z_scores[outliers_mask]  # Ajout du z-score
				extreme_vals['Type'] = extreme_vals['is_genuine'].map({1: 'Authentique', 0: 'Contrefait'}) # Mapping pour le type
				display(extreme_vals[[col, 'Z-score', 'Type']]
						.sort_values('Z-score', ascending=False)
						.head()
						.style.format({col: '{:.3f}', 'Z-score': '{:.3f}'})
						.hide(axis='index'))

	# Affichage du titre du résumé global
	display(pd.DataFrame({'': ['=== RÉSUMÉ GLOBAL DES VALEURS ABERRANTES ===']}).style.hide(axis='index'))

	# Identification de la variable la plus affectée par les valeurs aberrantes
	most_affected = max(outliers_by_var.items(), key=lambda x: x[1]['total'])

	# Création du tableau de résumé global
	summary = pd.DataFrame({
		'Aspect': [
			'Variable la plus affectée',
			'Répartition authentique/contrefait',
			'Impact sur la détection'
		],
		'Conclusion': [
			f"{most_affected[0]} ({most_affected[1]['total']} valeurs aberrantes)",
			f"Authentiques: {sum(v['authentiques'] for v in outliers_by_var.values())}, " \
			f"Contrefaits: {sum(v['contrefaits'] for v in outliers_by_var.values())}",
			"À utiliser comme indicateur potentiel de contrefaçon" if \
			sum(v['contrefaits'] for v in outliers_by_var.values()) > \
			sum(v['authentiques'] for v in outliers_by_var.values()) else \
			"Distribution équilibrée entre les types"
		]
	})

	# Affichage du résumé global
	display(summary.style.hide(axis='index'))

except Exception as e:
	# Gestion des erreurs et affichage du message d'erreur
	display(pd.DataFrame({'Erreur': [str(e)]}).style.hide(axis='index'))
	raise
=== ANALYSE DES VALEURS ABERRANTES (DataFrame original) ===
=== ANALYSE PAR VARIABLE ===
=== VARIABLE : diagonal ===
Métrique Valeur
Total valeurs aberrantes 6 (0.40%)
Dans billets authentiques 3 (0.30%)
Dans billets contrefaits 3 (0.60%)
Minimum (sans aberrants) 171.050
Maximum (sans aberrants) 172.760
Moyenne (sans aberrants) 171.957
Écart-type (sans aberrants) 0.300
Top 5 des valeurs aberrantes pour diagonal
diagonal Z-score Type
173.010 3.447 nan
172.920 3.152 nan
172.890 3.053 nan
172.890 3.053 nan
171.040 3.010 nan
=== VARIABLE : height_left ===
Métrique Valeur
Total valeurs aberrantes 0 (0.00%)
Dans billets authentiques 0 (0.00%)
Dans billets contrefaits 0 (0.00%)
Minimum (sans aberrants) 103.140
Maximum (sans aberrants) 104.880
Moyenne (sans aberrants) 104.030
Écart-type (sans aberrants) 0.299
=== VARIABLE : height_right ===
Métrique Valeur
Total valeurs aberrantes 4 (0.27%)
Dans billets authentiques 3 (0.30%)
Dans billets contrefaits 1 (0.20%)
Minimum (sans aberrants) 102.950
Maximum (sans aberrants) 104.870
Moyenne (sans aberrants) 103.920
Écart-type (sans aberrants) 0.322
Top 5 des valeurs aberrantes pour height_right
height_right Z-score Type
102.820 3.380 nan
104.950 3.163 nan
104.950 3.163 nan
102.910 3.104 nan
=== VARIABLE : margin_low ===
Métrique Valeur
Total valeurs aberrantes 0 (0.00%)
Dans billets authentiques 0 (0.00%)
Dans billets contrefaits 0 (0.00%)
Minimum (sans aberrants) 2.980
Maximum (sans aberrants) 6.900
Moyenne (sans aberrants) 4.486
Écart-type (sans aberrants) 0.664
=== VARIABLE : margin_up ===
Métrique Valeur
Total valeurs aberrantes 2 (0.13%)
Dans billets authentiques 1 (0.10%)
Dans billets contrefaits 1 (0.20%)
Minimum (sans aberrants) 2.560
Maximum (sans aberrants) 3.810
Moyenne (sans aberrants) 3.152
Écart-type (sans aberrants) 0.230
Top 5 des valeurs aberrantes pour margin_up
margin_up Z-score Type
2.270 3.804 nan
3.910 3.273 nan
=== VARIABLE : length ===
Métrique Valeur
Total valeurs aberrantes 3 (0.20%)
Dans billets authentiques 0 (0.00%)
Dans billets contrefaits 3 (0.60%)
Minimum (sans aberrants) 110.220
Maximum (sans aberrants) 114.440
Moyenne (sans aberrants) 112.684
Écart-type (sans aberrants) 0.864
Top 5 des valeurs aberrantes pour length
length Z-score Type
109.490 3.655 nan
109.930 3.150 nan
109.970 3.105 nan
=== RÉSUMÉ GLOBAL DES VALEURS ABERRANTES ===
Aspect Conclusion
Variable la plus affectée diagonal (6 valeurs aberrantes)
Répartition authentique/contrefait Authentiques: 7, Contrefaits: 8
Impact sur la détection À utiliser comme indicateur potentiel de contrefaçon

¶

3 - TRAITEMENT DES VALEURS MANQUANTES

¶

3.1 - Imputations avec l'ensemble des variables explicatives
In [ ]:
 
In [ ]:
 
In [ ]:
 
In [ ]:
 
In [ ]:
 
In [ ]:
 
In [ ]:
 

¶

3.1 - Imputations avec l'ensemble des variables explicatives
In [19]:
class ImputationTracker:
	def __init__(self, features):
		"""Initialisation avec toutes les métriques nécessaires pour le suivi complet"""
		self.features = features
		self.metrics_history = {
			'phase': [],
			# Métriques de performance directes
			'r2': [],
			'rmse': [],
			'mae': [],
			'explained_variance': [],
			'max_error': [],
			# Métriques de validation croisée
			'r2_cv_mean': [],
			'r2_cv_std': [],
			'rmse_cv_mean': [],
			'rmse_cv_std': [],
			# Tests statistiques
			'dw_test': [],
			'het_pvalue': [],
			'shapiro_pvalue': [],
			# Intervalles de confiance
			'interval_coverage': [],
			'interval_width': [],
			# Métriques d'outliers
			'mad_score_mean': [],
			'z_score_mean': []
		}
		self.hypotheses_results = {}
		self.best_model = None
		self.models = {}
    
	def add_new_phase(self, phase_name, metrics):
		"""Ajoute ou met à jour une phase avec toutes les métriques"""
		if phase_name in self.metrics_history['phase']:
			idx = self.metrics_history['phase'].index(phase_name)
			for key in metrics:
				if key in self.metrics_history:
					self.metrics_history[key][idx] = metrics[key]
		else:
			self.metrics_history['phase'].append(phase_name)
			for key in self.metrics_history:
				if key != 'phase':
					self.metrics_history[key].append(metrics.get(key, None))

	def get_summary(self):
		"""Retourne un DataFrame avec l'historique complet des métriques"""
		summary = pd.DataFrame(self.metrics_history)
		hypotheses_df = pd.DataFrame(self.hypotheses_results, index=[0])
		return pd.concat([summary, hypotheses_df], axis=1)

	def record_initial_data(self, df):
		"""Enregistre les informations initiales du DataFrame"""
		self.initial_data_shape = df.shape
		self.initial_data_types = df.dtypes

	def record_hypotheses_results(self, **kwargs):
		"""Enregistre les résultats des tests d'hypothèses"""
		self.hypotheses_results.update(kwargs)

	def record_metrics(self, phase, model=None, **metrics):
		"""Enregistre les métriques pour une phase donnée"""
		if model:
			self.models[phase] = model
		self.add_new_phase(phase, metrics)

	def get_best_model(self):
		"""Retourne le meilleur modèle"""
		return self.best_model

	def select_best_model(self):
		"""Sélectionne le meilleur modèle en fonction du R² CV"""
		with_outliers_metrics = self.metrics_history.get("with_outliers", {})
		without_outliers_metrics = self.metrics_history.get("without_outliers", {})

		if (with_outliers_metrics.get("r2_cv", -np.inf) >
			without_outliers_metrics.get("r2_cv", -np.inf)):
			self.best_model = self.models.get("with_outliers")
		else:
			self.best_model = self.models.get("without_outliers")
		
		return self.best_model

def calculate_robust_intervals(model, X, y, alpha=0.05):
	"""Calcule des intervalles de confiance robustes"""
	y_pred = model.predict(X)
	residuals = y - y_pred
	
	n = len(X)
	if X.ndim == 1:
		X = X.reshape(-1, 1)

	h = np.diagonal(X.dot(np.linalg.inv(X.T.dot(X))).dot(X.T))
	sigma = np.sqrt(residuals**2 / (1 - h))
	
	t_value = stats.t.ppf(1 - alpha/2, n-X.shape[1])
	margin = t_value * sigma
	
	return pd.DataFrame({
		'prediction': y_pred,
		'lower_bound': y_pred - margin,
		'upper_bound': y_pred + margin,
		'std_error': sigma
	})

def etape1(df):
	"""
	Préparation des données analyse initiale
	"""
	# === Préparation initiale ===
	display(pd.DataFrame({'': ['=== PRÉPARATION DES DONNÉES ET ANALYSE INITIALE ===']}).style.hide(axis='index'))

	# Conversion Dask si nécessaire
	if hasattr(df, 'compute'):
		df = df.compute()

	# Préparation des variables
	df['is_genuine'] = df['is_genuine'].astype(int)
	df['margin_low_missing'] = df['margin_low'].isna().astype(int)

	# Séparation train/test avec stratification
	X_train, X_test, y_train, y_test = train_test_split(
		df.drop('margin_low', axis=1),
		df['margin_low'],
		test_size=0.10,
		random_state=42,
		stratify=df[['is_genuine', 'margin_low_missing']]
	)

	# Préparation des données finales
	X_train = X_train.drop('margin_low_missing', axis=1)
	X_test = X_test.drop('margin_low_missing', axis=1)
	features = X_train.columns

	# === Rapport de préparation des données ===
	results = []

	# 1. Dimensions des données
	results.append({
		'Section': 'Dimensions des données',
		'Description': 'X_train',
		'Valeur': f"{X_train.shape[0]} observations, {X_train.shape[1]} variables"
	})
	results.append({
		'Section': 'Dimensions des données',
		'Description': 'X_test',
		'Valeur': f"{X_test.shape[0]} observations, {X_test.shape[1]} variables"
	})

	# 2. Valeurs manquantes dans X
	results.append({
		'Section': 'Valeurs manquantes - Variables explicatives',
		'Description': '',
		'Valeur': ''
	})

	for col in X_train.columns:
		results.append({
			'Section': 'X_train - Valeurs manquantes',
			'Description': col,
			'Valeur': f"{X_train[col].isna().sum()} ({(X_train[col].isna().sum() / len(X_train) * 100):.1f}%)"
		})
	
	for col in X_test.columns:
		results.append({
			'Section': 'X_test - Valeurs manquantes',
			'Description': col,
			'Valeur': f"{X_test[col].isna().sum()} ({(X_test[col].isna().sum() / len(X_test) * 100):.1f}%)"
		})

	# 3. Valeurs manquantes dans y
	results.append({
		'Section': 'Valeurs manquantes - Variable cible',
		'Description': 'y_train',
		'Valeur': f"{y_train.isna().sum()} NaN ({(y_train.isna().sum() / len(y_train) * 100):.1f}%)"
	})
	results.append({
		'Section': 'Valeurs manquantes - Variable cible',
		'Description': 'y_test',
		'Valeur': f"{y_test.isna().sum()} NaN ({(y_test.isna().sum() / len(y_test) * 100):.1f}%)"
	})

	# 4. Distribution des classes
	train_genuine = X_train['is_genuine'].value_counts(normalize=True)
	test_genuine = X_test['is_genuine'].value_counts(normalize=True)

	results.append({
		'Section': 'Distribution des classes - Train',
		'Description': 'Authentique (1)',
		'Valeur': f"{train_genuine[1]:.1%} ({X_train['is_genuine'].value_counts()[1]} obs.)"
	})
	results.append({
		'Section': 'Distribution des classes - Train',
		'Description': 'Contrefait (0)',
		'Valeur': f"{train_genuine[0]:.1%} ({X_train['is_genuine'].value_counts()[0]} obs.)"
	})
	results.append({
		'Section': 'Distribution des classes - Test',
		'Description': 'Authentique (1)',
		'Valeur': f"{test_genuine[1]:.1%} ({X_test['is_genuine'].value_counts()[1]} obs.)"
	})
	results.append({
		'Section': 'Distribution des classes - Test',
		'Description': 'Contrefait (0)',
		'Valeur': f"{test_genuine[0]:.1%} ({X_test['is_genuine'].value_counts()[0]} obs.)"
	})

	# Affichage du rapport de préparation
	prep_df = pd.DataFrame(results)
	display(prep_df.style.hide(axis='index'))

	# === Analyse initiale et métriques ===
	display(pd.DataFrame({'': ['=== MÉTRIQUES INITIALES ===']}).style.hide(axis='index'))

	# Initialisation du tracker
	imputation_tracker = ImputationTracker(features)
	imputation_tracker.record_initial_data(df)

	# Préparation pour analyse initiale
	df_base = df.dropna(subset=['margin_low'])
	X_base = df_base.drop(['margin_low', 'margin_low_missing'], axis=1)
	y_base = df_base['margin_low']

	# Configuration de la validation croisée
	cv = KFold(n_splits=10, shuffle=True, random_state=42)

	# Calcul des métriques CV
	cv_r2_scores = cross_val_score(LinearRegression(), X_base, y_base, cv=cv, scoring='r2')
	cv_rmse_scores = np.sqrt(-cross_val_score(LinearRegression(), X_base, y_base, 
			cv=cv, scoring='neg_mean_squared_error'))

	# Modèle initial et prédictions
	base_model = LinearRegression()
	base_model.fit(X_base, y_base)
	y_pred_base = base_model.predict(X_base)

	# Calcul des résidus
	residuals = y_base - y_pred_base
	X_stats = sm.add_constant(X_base)

	# Tests statistiques
	dw_test = durbin_watson(residuals)
	_, het_pvalue, _, _ = het_breuschpagan(residuals, X_stats)
	_, shapiro_pvalue = stats.shapiro(residuals)

	# Analyses des outliers
	z_scores = np.abs(stats.zscore(residuals))
	mad_scores = np.abs(residuals - np.median(residuals)) / stats.median_abs_deviation(residuals)

	# Intervalles de confiance
	base_intervals = calculate_robust_intervals(base_model, X_base.values, y_base)

	# Enregistrement des métriques initiales
	initial_metrics = {
		# Métriques de performance directes
		'r2': r2_score(y_base, y_pred_base),
		'rmse': np.sqrt(mean_squared_error(y_base, y_pred_base)),
		'mae': mean_absolute_error(y_base, y_pred_base),
		'explained_variance': explained_variance_score(y_base, y_pred_base),
		'max_error': max_error(y_base, y_pred_base),
		# Métriques de validation croisée
		'r2_cv_mean': cv_r2_scores.mean(),
		'r2_cv_std': cv_r2_scores.std(),
		'rmse_cv_mean': cv_rmse_scores.mean(),
		'rmse_cv_std': cv_rmse_scores.std(),
		# Tests statistiques
		'dw_test': dw_test,
		'het_pvalue': het_pvalue,
		'shapiro_pvalue': shapiro_pvalue,
		# Intervalles de confiance
		'interval_coverage': np.mean(
			(y_base >= base_intervals['lower_bound']) &
			(y_base <= base_intervals['upper_bound'])
		),
		'interval_width': np.mean(
			base_intervals['upper_bound'] - base_intervals['lower_bound']
		),
		# Métriques d'outliers
		'mad_score_mean': np.mean(mad_scores),
		'z_score_mean': np.mean(z_scores)
	}

	# Enregistrement dans le tracker
	imputation_tracker.add_new_phase('initial', initial_metrics)
	imputation_tracker.record_metrics('initial', base_model, **initial_metrics)

	# Création du DataFrame de résultats avec interprétations
	results_df = pd.DataFrame({
		'Métrique': [
			'R² direct',
			'R² CV (moyenne ± écart-type)',
			'RMSE direct',
			'RMSE CV (moyenne ± écart-type)',
			'MAE',
			'Variance expliquée',
			'Erreur maximale',
			'Test Durbin-Watson',
			'P-value hétéroscédasticité',
			'P-value normalité',
			'Couverture des intervalles',
			'Largeur moyenne des intervalles',
			'Score MAD moyen',
			'Score Z moyen'
		],
		'Valeur': [
			f"{initial_metrics['r2']:.4f}",
			f"{initial_metrics['r2_cv_mean']:.4f} ± {initial_metrics['r2_cv_std']:.4f}",
			f"{initial_metrics['rmse']:.4f}",
			f"{initial_metrics['rmse_cv_mean']:.4f} ± {initial_metrics['rmse_cv_std']:.4f}",
			f"{initial_metrics['mae']:.4f}",
			f"{initial_metrics['explained_variance']:.4f}",
			f"{initial_metrics['max_error']:.4f}",
			f"{initial_metrics['dw_test']:.4f}",
			f"{initial_metrics['het_pvalue']:.2e}",
			f"{initial_metrics['shapiro_pvalue']:.2e}",
			f"{initial_metrics['interval_coverage']:.2%}",
			f"{initial_metrics['interval_width']:.4f}",
			f"{initial_metrics['mad_score_mean']:.4f}",
			f"{initial_metrics['z_score_mean']:.4f}"
		],
		'Interprétation': [
			'Qualité d\'ajustement directe',
			'Performance moyenne et stabilité (CV)',
			'Erreur quadratique moyenne directe',
			'RMSE moyen et stabilité (CV)',
			'Erreur absolue moyenne',
			'Variance expliquée par le modèle',
			'Plus grande erreur observée',
			'Indépendant' if 1.5 < initial_metrics['dw_test'] < 2.5 else 'Dépendant',
			'Homoscédastique' if initial_metrics['het_pvalue'] > 0.05 else 'Hétéroscédastique',
			'Normal' if initial_metrics['shapiro_pvalue'] > 0.05 else 'Non normal',
			'Proportion dans les intervalles',
			'Précision des estimations',
			'Dispersion robuste (MAD)',
			'Dispersion (Z-score)'
		]
	})

	# Affichage des résultats
	display(results_df.style.hide(axis='index'))

	return X_train, X_test, y_train, y_test, imputation_tracker

if __name__ == "__main__":
	# Exemple d'utilisation
	X_train, X_test, y_train, y_test, tracker = etape1(df)
	
	# Récupération du résumé complet
	summary_df = tracker.get_summary()
=== PRÉPARATION DES DONNÉES ET ANALYSE INITIALE ===
Section Description Valeur
Dimensions des données X_train 1350 observations, 6 variables
Dimensions des données X_test 150 observations, 6 variables
Valeurs manquantes - Variables explicatives
X_train - Valeurs manquantes is_genuine 0 (0.0%)
X_train - Valeurs manquantes diagonal 0 (0.0%)
X_train - Valeurs manquantes height_left 0 (0.0%)
X_train - Valeurs manquantes height_right 0 (0.0%)
X_train - Valeurs manquantes margin_up 0 (0.0%)
X_train - Valeurs manquantes length 0 (0.0%)
X_test - Valeurs manquantes is_genuine 0 (0.0%)
X_test - Valeurs manquantes diagonal 0 (0.0%)
X_test - Valeurs manquantes height_left 0 (0.0%)
X_test - Valeurs manquantes height_right 0 (0.0%)
X_test - Valeurs manquantes margin_up 0 (0.0%)
X_test - Valeurs manquantes length 0 (0.0%)
Valeurs manquantes - Variable cible y_train 33 NaN (2.4%)
Valeurs manquantes - Variable cible y_test 4 NaN (2.7%)
Distribution des classes - Train Authentique (1) 66.7% (900 obs.)
Distribution des classes - Train Contrefait (0) 33.3% (450 obs.)
Distribution des classes - Test Authentique (1) 66.7% (100 obs.)
Distribution des classes - Test Contrefait (0) 33.3% (50 obs.)
=== MÉTRIQUES INITIALES ===
Métrique Valeur Interprétation
R² direct 0.6169 Qualité d'ajustement directe
R² CV (moyenne ± écart-type) 0.6069 ± 0.0489 Performance moyenne et stabilité (CV)
RMSE direct 0.4107 Erreur quadratique moyenne directe
RMSE CV (moyenne ± écart-type) 0.4118 ± 0.0289 RMSE moyen et stabilité (CV)
MAE 0.3158 Erreur absolue moyenne
Variance expliquée 0.6169 Variance expliquée par le modèle
Erreur maximale 1.6882 Plus grande erreur observée
Test Durbin-Watson 2.0385 Indépendant
P-value hétéroscédasticité 3.46e-33 Hétéroscédastique
P-value normalité 7.06e-06 Non normal
Couverture des intervalles 100.00% Proportion dans les intervalles
Largeur moyenne des intervalles 1.2417 Précision des estimations
Score MAD moyen 1.2119 Dispersion robuste (MAD)
Score Z moyen 0.7689 Dispersion (Z-score)
In [20]:
# 2 - Vérification des hypothèses de la régression linéaire
display(pd.DataFrame({'': ['=== VÉRIFICATION DES HYPOTHÈSES DE LA RÉGRESSION LINÉAIRE ===']}).style.hide(axis='index'))

try:
    # Conversion et préparation des données
    X_train_stats = X_train.astype(float)
    y_train_stats = y_train.astype(float)

    # Nettoyage des valeurs manquantes pour l'analyse statistique
    mask_stats = ~(X_train_stats.isna().any(axis=1) | y_train_stats.isna())
    X_train_stats = X_train_stats[mask_stats]
    y_train_stats = y_train_stats[mask_stats]

    # 1. Test de linéarité
    display(pd.DataFrame({'': ['=== TEST DE LINÉARITÉ ===']}).style.hide(axis='index'))
    
    for col in X_train_stats.columns:
        plt.figure(figsize=(12, 4))
        
        # Relation avec la variable cible
        plt.subplot(121)
        sns.regplot(x=X_train_stats[col], y=y_train_stats, scatter_kws={'alpha':0.5}, line_kws={'color': 'red'})
        plt.title(f'Relation {col} vs {y_train_stats.name}')
        
        # Distribution des valeurs
        plt.subplot(122)
        sns.boxplot(x=X_train_stats[col])
        plt.title(f'Distribution de {col}')
        
        corr = np.corrcoef(X_train_stats[col], y_train_stats)[0,1]
        plt.suptitle(f'Analyse de {col} (correlation: {corr:.3f})')
        plt.tight_layout()
        plt.show()

    # Création du modèle préliminaire pour les résidus
    X_const = sm.add_constant(X_train_stats)
    model_prelim = sm.OLS(y_train_stats, X_const).fit()
    residuals = model_prelim.resid

    # Nettoyage des données pour les tests
    valid_fitted = model_prelim.fittedvalues[~np.isnan(model_prelim.fittedvalues)]
    valid_residuals = residuals[~np.isnan(residuals)]
    valid_X = X_train_stats.dropna()

    # 2. Test d'indépendance (Durbin-Watson)
    display(pd.DataFrame({'': ['=== TEST DE DURBIN-WATSON ===']}).style.hide(axis='index'))
    
    dw_test = durbin_watson(residuals)
    display(pd.DataFrame({
        'Test': ['Durbin-Watson'],
        'Statistique': [dw_test],
        'Interprétation': ['Indépendant' if 1.5 < dw_test < 2.5 else 'Dépendant']
    }).style.hide(axis='index'))

    # 3. Test d'homoscédasticité
    display(pd.DataFrame({'': ['=== TEST D\'HOMOSCÉDASTICITÉ ===']}).style.hide(axis='index'))

    # Test de Breusch-Pagan
    try:
        _, het_p_value, _, _ = het_breuschpagan(valid_residuals, sm.add_constant(valid_X))
        df_bp = pd.DataFrame({
            'Test': ['Breusch-Pagan'],
            'P-value': [het_p_value],
            'Interprétation': ['Homoscédastique' if het_p_value > 0.05 else 'Hétéroscédastique']
        })
        display(df_bp.style.format({'P-value': '{:.2e}'}).hide(axis='index'))
    except:
        display("Impossible de calculer le test de Breusch-Pagan")

    # 4. Test de normalité des résidus
    display(pd.DataFrame({'': ['=== TEST DE NORMALITÉ DES RÉSIDUS ===']}).style.hide(axis='index'))
    
    # Graphiques combinés (QQ plot, homoscédasticité, autocorrélation)
    plt.figure(figsize=(15, 5))

    # Homoscédasticité
    plt.subplot(131)
    plt.scatter(valid_fitted, valid_residuals, alpha=0.5)
    plt.axhline(y=0, color='r', linestyle='--')
    plt.xlabel('Valeurs prédites')
    plt.ylabel('Résidus')
    plt.title('Graphique d\'homoscédasticité')

    # Q-Q plot
    plt.subplot(132)
    stats.probplot(valid_residuals, dist="norm", plot=plt)
    plt.title("Q-Q Plot des résidus")

    # Autocorrélation
    plt.subplot(133)
    sm.graphics.tsa.plot_acf(valid_residuals, lags=40, alpha=0.05, ax=plt.gca())
    plt.title('Autocorrélation des résidus')

    plt.tight_layout()
    plt.show()

    # Test de Shapiro-Wilk
    if len(valid_residuals) >= 3:
        shapiro_stat, shapiro_p = stats.shapiro(valid_residuals)
        display(pd.DataFrame({
            'Test': ['Shapiro-Wilk'],
            'Statistique': [shapiro_stat],
            'P-valeur': [shapiro_p],
            'Interprétation': ['Normal' if shapiro_p > 0.05 else 'Non normal']
        }).style.hide(axis='index'))

    # 5. Test de multicolinéarité
    display(pd.DataFrame({'': ['=== TEST DE MULTICOLINÉARITÉ ===']}).style.hide(axis='index'))
    
    if X_train_stats.shape[1] > 0 and not valid_X.empty:
        vif_data = pd.DataFrame()
        vif_data["Variable"] = X_train_stats.columns
        vif_data["VIF"] = [variance_inflation_factor(X_train_stats.values, i)
            for i in range(X_train_stats.shape[1])]
        
        # Ajout de l'interprétation des VIF
        vif_data['Interprétation'] = pd.cut(
            vif_data['VIF'],
            bins=[0, 2, 5, 10, float('inf')],
            labels=['Faible', 'Modérée', 'Élevée', 'Très élevée']
        )
        display(vif_data.sort_values('VIF', ascending=False).style.hide(axis='index'))

    # 6. Analyse des résidus aberrants
    display(pd.DataFrame({'': ['=== ANALYSE DES RÉSIDUS ABERRANTS ===']}).style.hide(axis='index'))
    
    # Calcul des Z-scores et MAD scores des résidus
    z_scores = np.abs(stats.zscore(valid_residuals))
    mad_scores = np.abs(residuals - np.median(residuals)) / stats.median_abs_deviation(residuals)
    
    plt.figure(figsize=(15, 5))
    
    # Distribution des résidus
    plt.subplot(131)
    sns.histplot(residuals, kde=True)
    plt.axvline(x=0, color='r', linestyle='--')
    plt.title('Distribution des résidus')
    
    # Z-scores
    plt.subplot(132)
    sns.histplot(z_scores, kde=True)
    plt.axvline(x=3, color='r', linestyle='--', label='Seuil Z-score = 3')
    plt.title(f'Z-scores (moyenne: {np.mean(z_scores):.3f})')
    plt.legend()
    
    # MAD scores
    plt.subplot(133)
    sns.histplot(mad_scores, kde=True)
    plt.axvline(x=3.5, color='r', linestyle='--', label='Seuil MAD = 3.5')
    plt.title(f'Scores MAD (moyenne: {np.mean(mad_scores):.3f})')
    plt.legend()
    
    plt.tight_layout()
    plt.show()

    # 7. Résumé global
    display(pd.DataFrame({'': ['=== RÉSUMÉ GLOBAL ===']}).style.hide(axis='index'))

    hypotheses_summary = pd.DataFrame({
        'Hypothèse': [
            'Linéarité',
            'Indépendance',
            'Homoscédasticité',
            'Normalité',
            'Non-multicolinéarité',
            'Outliers'
        ],
        'Test': [
            'Visuel',
            'Durbin-Watson',
            'Breusch-Pagan',
            'Shapiro-Wilk',
            'VIF',
            'Z-score & MAD'
        ],
        'Résultat': [
            'À vérifier',
            f"{dw_test:.3f}",
            f"{het_p_value:.3e}",
            f"{shapiro_p:.3e}",
            f"Max: {vif_data['VIF'].max():.3f}",
            f"Z-score > 3: {((z_scores > 3).mean())*100:.1f}%, MAD > 3.5: {((mad_scores > 3.5).mean())*100:.1f}%"
        ],
        'Validée': [
            'Vérifier visuellement',
            'Oui' if 1.5 < dw_test < 2.5 else 'Non',
            'Oui' if het_p_value > 0.05 else 'Non',
            'Oui' if shapiro_p > 0.05 else 'Non',
            'Voir les VIF',
            f"Outliers potentiels: {((z_scores > 3).mean() + (mad_scores > 3.5).mean())/2*100:.1f}%"
        ]
    })
    display(hypotheses_summary.style.hide(axis='index'))

except Exception as e:
    display(pd.DataFrame({'Erreur': [str(e)]}).style.hide(axis='index'))
    raise
=== VÉRIFICATION DES HYPOTHÈSES DE LA RÉGRESSION LINÉAIRE ===
=== TEST DE LINÉARITÉ ===
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
=== TEST DE DURBIN-WATSON ===
Test Statistique Interprétation
Durbin-Watson 1.997567 Indépendant
=== TEST D'HOMOSCÉDASTICITÉ ===
Test P-value Interprétation
Breusch-Pagan 5.52e-32 Hétéroscédastique
=== TEST DE NORMALITÉ DES RÉSIDUS ===
No description has been provided for this image
Test Statistique P-valeur Interprétation
Shapiro-Wilk 0.993250 0.000010 Non normal
=== TEST DE MULTICOLINÉARITÉ ===
Variable VIF Interprétation
diagonal 172777.440602 Très élevée
height_left 119772.157146 Très élevée
height_right 113548.896398 Très élevée
length 54686.326486 Très élevée
margin_up 292.488517 Très élevée
is_genuine 13.816818 Très élevée
=== ANALYSE DES RÉSIDUS ABERRANTS ===
No description has been provided for this image
=== RÉSUMÉ GLOBAL ===
Hypothèse Test Résultat Validée
Linéarité Visuel À vérifier Vérifier visuellement
Indépendance Durbin-Watson 1.998 Oui
Homoscédasticité Breusch-Pagan 5.520e-32 Non
Normalité Shapiro-Wilk 1.043e-05 Non
Non-multicolinéarité VIF Max: 172777.441 Voir les VIF
Outliers Z-score & MAD Z-score > 3: 0.9%, MAD > 3.5: 3.6% Outliers potentiels: 2.3%
In [21]:
# 3 Optimisation et entraînement du modèle sur les données du train set
def get_optimal_splits(n_samples):
    """
    Détermine le nombre optimal de plis pour la validation croisée en fonction du nombre d'échantillons.
    """
    if n_samples < 1000:
        return 5
    elif n_samples < 10000:
        return 10
    else:
        return 20

def train_and_evaluate_model(X, y, title=""):
    """
    Entraîne et évalue un modèle.
    """
    # Configuration du pipeline
    pipeline = Pipeline([
        ('scaler', RobustScaler()),
        ('model', LinearRegression())
    ])

    # Paramètres pour GridSearchCV
    param_grid = {
        'scaler__quantile_range': [(25.0, 75.0), (10.0, 90.0), (5.0, 95.0)],
        'model__fit_intercept': [True, False],
        'model__positive': [False, True],
        'model': [LinearRegression(), Ridge(), Lasso()]
    }

    # Configuration de la validation croisée
    n_splits = get_optimal_splits(len(X))
    cv = KFold(n_splits=n_splits, shuffle=True, random_state=42)

    # Configuration et entraînement GridSearchCV
    grid_search = GridSearchCV(
        estimator=pipeline,
        param_grid=param_grid,
        scoring={'r2': 'r2', 'neg_mae': 'neg_mean_absolute_error'},
        cv=cv,
        return_train_score=True,
        refit='neg_mae',
        verbose=1
    )
    grid_search.fit(X, y)
    best_model = grid_search.best_estimator_

    # Calcul des prédictions
    y_pred = best_model.predict(X)
    residuals = y - y_pred
    
    # Calcul des métriques principales
    r2 = r2_score(y, y_pred)
    rmse = np.sqrt(mean_squared_error(y, y_pred))
    mae = mean_absolute_error(y, y_pred)
    explained_var = explained_variance_score(y, y_pred)
    max_err = max_error(y, y_pred)

    # Validation croisée
    cv_scores = cross_val_score(best_model, X, y, cv=cv, scoring='r2')

    return {
        'model': best_model,
        'r2': r2,
        'r2_cv_mean': cv_scores.mean(),
        'r2_cv_std': cv_scores.std(),
        'rmse': rmse,
        'mae': mae,
        'explained_variance': explained_var,
        'max_error': max_err,
        'params': grid_search.best_params_
    }

# === OPTIMISATION ET ENTRAÎNEMENT ===
display(pd.DataFrame({'': ['=== OPTIMISATION ET ENTRAÎNEMENT ===']}).style.hide(axis='index'))

# Nettoyage initial des données
display(pd.DataFrame({'': ["=== NETTOYAGE INITIAL DES DONNÉES ==="]}).style.hide(axis='index'))
display(pd.DataFrame({
    'Caractéristique': ['Shape initial X_train', 'Shape initial y_train'],
    'Valeur': [str(X_train.shape), str(y_train.shape)]
}).style.hide(axis='index'))

# Copie et nettoyage minimal des données
X_train_clean = X_train.copy()
y_train_clean = y_train.copy()

# Alignement des index
common_index = X_train_clean.index.intersection(y_train_clean.index)
X_train_clean = X_train_clean.loc[common_index]
y_train_clean = y_train_clean.loc[common_index]

# Retrait des NaN
mask = ~(X_train_clean.isna().any(axis=1) | y_train_clean.isna())
X_train_clean = X_train_clean[mask]
y_train_clean = y_train_clean[mask]

display(pd.DataFrame({'': ["=== ENTRAÎNEMENT AVEC TOUTES LES DONNÉES ==="]}).style.hide(axis='index'))
results_with_outliers = train_and_evaluate_model(X_train_clean, y_train_clean)

# Enregistrement des métriques avec outliers
tracker.record_metrics(
    'with_outliers',
    results_with_outliers['model'],
    r2=results_with_outliers['r2'],
    r2_cv_mean=results_with_outliers['r2_cv_mean'],
    r2_cv_std=results_with_outliers['r2_cv_std'],
    rmse=results_with_outliers['rmse'],
    mae=results_with_outliers['mae'],
    explained_variance=results_with_outliers['explained_variance'],
    max_error=results_with_outliers['max_error']
)

# Identification des valeurs aberrantes
residuals = y_train_clean - results_with_outliers['model'].predict(X_train_clean)
z_scores = np.abs(stats.zscore(residuals))
outliers_mask = z_scores > 3

# Création du jeu de données sans outliers
X_train_no_outliers = X_train_clean[~outliers_mask]
y_train_no_outliers = y_train_clean[~outliers_mask]

display(pd.DataFrame({'': ['=== ANALYSE DES VALEURS ABERRANTES ===']}).style.hide(axis='index'))
display(pd.DataFrame({
    'Caractéristique': [
        'Nombre total de valeurs aberrantes',
        'Pourcentage de valeurs aberrantes',
        'Nombre d\'observations restantes'
    ],
    'Valeur': [
        f"{outliers_mask.sum()}",
        f"{(outliers_mask.sum() / len(X_train_clean) * 100):.2f}%",
        f"{len(X_train_no_outliers)}"
    ]
}).style.hide(axis='index'))

display(pd.DataFrame({'': ["=== ENTRAÎNEMENT SANS VALEURS ABERRANTES ==="]}).style.hide(axis='index'))
results_without_outliers = train_and_evaluate_model(X_train_no_outliers, y_train_no_outliers)

# Enregistrement des métriques sans outliers
tracker.record_metrics(
    'without_outliers',
    results_without_outliers['model'],
    r2=results_without_outliers['r2'],
    r2_cv_mean=results_without_outliers['r2_cv_mean'],
    r2_cv_std=results_without_outliers['r2_cv_std'],
    rmse=results_without_outliers['rmse'],
    mae=results_without_outliers['mae'],
    explained_variance=results_without_outliers['explained_variance'],
    max_error=results_without_outliers['max_error']
)

# Comparaison des performances
display(pd.DataFrame({'': ["=== COMPARAISON DES PERFORMANCES ==="]}).style.hide(axis='index'))

comparison = pd.DataFrame({
    'Métrique': [
        'R² (ensemble complet)',
        'R² validation croisée',
        'Intervalle de confiance R² (95%)',
        'RMSE',
        'MAE',
        'Variance expliquée',
        'Erreur maximale',
        'Nombre d\'observations'
    ],
    'Avec outliers': [
        f"{results_with_outliers['r2']:.4f}",
        f"{results_with_outliers['r2_cv_mean']:.4f}",
        f"[{results_with_outliers['r2_cv_mean'] - 2*results_with_outliers['r2_cv_std']:.4f}, "
        f"{results_with_outliers['r2_cv_mean'] + 2*results_with_outliers['r2_cv_std']:.4f}]",
        f"{results_with_outliers['rmse']:.4f}",
        f"{results_with_outliers['mae']:.4f}",
        f"{results_with_outliers['explained_variance']:.4f}",
        f"{results_with_outliers['max_error']:.4f}",
        len(X_train_clean)
    ],
    'Sans outliers': [
        f"{results_without_outliers['r2']:.4f}",
        f"{results_without_outliers['r2_cv_mean']:.4f}",
        f"[{results_without_outliers['r2_cv_mean'] - 2*results_without_outliers['r2_cv_std']:.4f}, "
        f"{results_without_outliers['r2_cv_mean'] + 2*results_without_outliers['r2_cv_std']:.4f}]",
        f"{results_without_outliers['rmse']:.4f}",
        f"{results_without_outliers['mae']:.4f}",
        f"{results_without_outliers['explained_variance']:.4f}",
        f"{results_without_outliers['max_error']:.4f}",
        len(X_train_no_outliers)
    ]
})
display(comparison.style.hide(axis='index'))

# Choix du meilleur modèle basé sur le MAE et le R²
seuil = 0.1  # Définir un seuil pour la différence de MAE

if (results_with_outliers['mae'] < results_without_outliers['mae'] and 
    results_with_outliers['r2'] > results_without_outliers['r2']):
    best_model = results_with_outliers['model']
    best_version = "avec outliers"
elif (results_without_outliers['mae'] < results_with_outliers['mae'] and 
        results_without_outliers['r2'] > results_with_outliers['r2']):
    best_model = results_without_outliers['model']
    best_version = "sans outliers"
else:
    if abs(results_with_outliers['mae'] - results_without_outliers['mae']) < seuil:
        if results_with_outliers['r2'] > results_without_outliers['r2']:
            best_model = results_with_outliers['model']
            best_version = "avec outliers"
        else:
            best_model = results_without_outliers['model']
            best_version = "sans outliers"
    else:
        if results_with_outliers['mae'] < results_without_outliers['mae']:
            best_model = results_with_outliers['model']
            best_version = "avec outliers"
        else:
            best_model = results_without_outliers['model']
            best_version = "sans outliers"

# Enregistrement du meilleur modèle dans le tracker
tracker.best_model = best_model

display(pd.DataFrame({'': ["=== CONCLUSION ==="]}).style.hide(axis='index'))
display(pd.DataFrame({
    'Aspect': [
        'Meilleure version',
        'R² validation croisée',
        'Amélioration relative'
    ],
    'Résultat': [
        best_version,
        f"{max(results_with_outliers['r2_cv_mean'], results_without_outliers['r2_cv_mean']):.4f}",
        f"{((results_without_outliers['r2_cv_mean'] - results_with_outliers['r2_cv_mean'])/results_with_outliers['r2_cv_mean'] * 100):.2f}%"
    ]
}).style.hide(axis='index'))
=== OPTIMISATION ET ENTRAÎNEMENT ===
=== NETTOYAGE INITIAL DES DONNÉES ===
Caractéristique Valeur
Shape initial X_train (1350, 6)
Shape initial y_train (1350,)
=== ENTRAÎNEMENT AVEC TOUTES LES DONNÉES ===
Fitting 10 folds for each of 36 candidates, totalling 360 fits
=== ANALYSE DES VALEURS ABERRANTES ===
Caractéristique Valeur
Nombre total de valeurs aberrantes 12
Pourcentage de valeurs aberrantes 0.91%
Nombre d'observations restantes 1305
=== ENTRAÎNEMENT SANS VALEURS ABERRANTES ===
Fitting 10 folds for each of 36 candidates, totalling 360 fits
=== COMPARAISON DES PERFORMANCES ===
Métrique Avec outliers Sans outliers
R² (ensemble complet) 0.6180 0.6335
R² validation croisée 0.6075 0.6158
Intervalle de confiance R² (95%) [0.5457, 0.6693] [0.4829, 0.7486]
RMSE 0.4129 0.3927
MAE 0.3174 0.3074
Variance expliquée 0.6180 0.6335
Erreur maximale 1.6756 1.2432
Nombre d'observations 1317 1305
=== CONCLUSION ===
Aspect Résultat
Meilleure version sans outliers
R² validation croisée 0.6158
Amélioration relative 1.36%
In [22]:
# 4 - Évaluation du modèle optimisé et gestion des valeurs manquantes

def get_optimal_splits(n_samples):
    """Détermine le nombre optimal de splits pour la validation croisée"""
    return min(5, max(2, n_samples // 30))

# Initialisation du tracker
features = X_train.columns
imputation_tracker = ImputationTracker(features)

# Sauvegarde d'une copie du DataFrame original avant imputation
df_original = df.copy()

display(pd.DataFrame({'': ['=== ÉVALUATION ET GESTION DES VALEURS MANQUANTES ===']}).style.hide(axis='index'))

# Vérification des données disponibles pour l'imputation
missing_count = df_original['margin_low'].isna().sum()
print(f"\nNombre de valeurs manquantes dans margin_low: {missing_count}")

# Combiner X_test et y_test pour supprimer les lignes avec des NaN
test_data = pd.concat([X_test, y_test], axis=1).dropna()
X_test_clean = test_data.drop(columns=y_test.name)
y_test_clean = test_data[y_test.name]

# Vérifier qu'il reste des données après le nettoyage
if len(X_test_clean) == 0:
    print("\nAucune donnée non manquante dans l'ensemble de test pour l'évaluation.")
else:
    # Prédiction sur l'ensemble de test nettoyé
    y_pred_test = best_model.predict(X_test_clean)

    # Configuration de la validation croisée
    n_splits = get_optimal_splits(len(X_test_clean))
    cv = KFold(n_splits=n_splits, shuffle=True, random_state=42)

    # Calcul des métriques sur l'ensemble de test avec validation croisée
    r2_scores = cross_val_score(best_model, X_test_clean, y_test_clean, cv=cv, scoring='r2')
    rmse_scores = np.sqrt(-cross_val_score(best_model, X_test_clean, y_test_clean, cv=cv, 
                                            scoring='neg_mean_squared_error'))

    test_metrics_cv = {
        'r2': r2_score(y_test_clean, y_pred_test),
        'rmse': np.sqrt(mean_squared_error(y_test_clean, y_pred_test)),
        'mae': mean_absolute_error(y_test_clean, y_pred_test),
        'explained_variance': explained_variance_score(y_test_clean, y_pred_test),
        'max_error': max_error(y_test_clean, y_pred_test),
        'r2_cv_mean': r2_scores.mean(),
        'r2_cv_std': r2_scores.std(),
        'rmse_cv_mean': rmse_scores.mean(),
        'rmse_cv_std': rmse_scores.std()
    }

    # Enregistrer les métriques dans le tracker
    imputation_tracker.record_metrics('test_cv', best_model, **test_metrics_cv)

    # === Analyse des résidus sur l'ensemble de test ===
    residuals_test = y_test_clean - y_pred_test
    X_test_stats = sm.add_constant(X_test_clean)
    dw_test = durbin_watson(residuals_test)
    _, pvalue_het_test, _, _ = het_breuschpagan(residuals_test, X_test_stats)
    _, pvalue_shapiro_test = stats.shapiro(residuals_test)

    # Enregistrer les résultats des tests d'hypothèses
    imputation_tracker.record_hypotheses_results(
        dw_test=dw_test,
        het_pvalue=pvalue_het_test,
        shapiro_pvalue=pvalue_shapiro_test
    )

    # Afficher les résultats des tests d'hypothèses
    hypotheses_results = pd.DataFrame([
        {'Test': 'Durbin-Watson', 
            'Statistique': f"{dw_test:.3f}",
            'Interprétation': 'Indépendant' if 1.5 < dw_test < 2.5 else 'Potentiellement dépendant'},
        {'Test': 'Hétéroscédasticité (Breusch-Pagan)', 
            'Statistique': f"{pvalue_het_test:.2e}",
            'Interprétation': 'Homoscédastique' if pvalue_het_test > 0.05 else 'Hétéroscédastique'},
        {'Test': 'Normalité (Shapiro-Wilk)', 
            'Statistique': f"{pvalue_shapiro_test:.2e}",
            'Interprétation': 'Normal' if pvalue_shapiro_test > 0.05 else 'Non-normal'}
    ])
    display(hypotheses_results.style.hide(axis='index'))

# === Imputation des valeurs manquantes ===
print("\n=== IMPUTATION DES VALEURS MANQUANTES ===")

# Identification des valeurs à imputer depuis le DataFrame original
X_missing = df_original[df_original['margin_low'].isna()].drop(['margin_low', 'margin_low_missing'], axis=1)
print(f"\nNombre de valeurs à imputer : {len(X_missing)}")

if X_missing.empty:
    print("Aucune valeur manquante à imputer dans 'margin_low'.")
else:
    try:
        # Imputation avec le meilleur modèle
        imputed_values = pd.Series(best_model.predict(X_missing), index=X_missing.index)
        df.loc[X_missing.index, 'margin_low'] = imputed_values

        print(f"Imputation réussie pour {len(imputed_values)} valeurs manquantes.")

        # Calcul des intervalles de confiance robustes
        intervals_missing = calculate_robust_intervals(best_model, X_missing.values, imputed_values)

        # Calcul de la couverture des intervalles et de la largeur moyenne
        interval_coverage = np.mean(
            (intervals_missing['lower_bound'] <= imputed_values) &
            (imputed_values <= intervals_missing['upper_bound'])
        )
        interval_width = np.mean(intervals_missing['upper_bound'] - intervals_missing['lower_bound'])

        # Enregistrer les métriques d'imputation
        imputation_metrics = {
            'interval_coverage': interval_coverage,
            'interval_width': interval_width
        }
        imputation_tracker.record_metrics('imputation', **imputation_metrics)

        # Afficher les résultats de l'imputation
        results_imputation = pd.DataFrame([
            {'Métrique': 'Couverture des intervalles', 'Valeur': f"{interval_coverage:.2%}"},
            {'Métrique': 'Largeur moyenne des intervalles', 'Valeur': f"{interval_width:.4f}"}
        ])
        display(results_imputation.style.hide(axis='index'))
        
    except Exception as e:
        print(f"Erreur lors de l'imputation : {str(e)}")

# Création d'une copie avec les données imputées pour la partie 5
df_imputed = df.copy()
=== ÉVALUATION ET GESTION DES VALEURS MANQUANTES ===
Nombre de valeurs manquantes dans margin_low: 37
Test Statistique Interprétation
Durbin-Watson 2.175 Indépendant
Hétéroscédasticité (Breusch-Pagan) 1.45e-01 Homoscédastique
Normalité (Shapiro-Wilk) 4.95e-01 Normal
=== IMPUTATION DES VALEURS MANQUANTES ===

Nombre de valeurs à imputer : 37
Imputation réussie pour 37 valeurs manquantes.
Métrique Valeur
Couverture des intervalles 100.00%
Largeur moyenne des intervalles 0.0000
In [23]:
# === PARTIE 5 : ÉVALUATION ET IMPUTATION DES VALEURS MANQUANTES ===

def partie_5(X_train, X_test, y_train, y_test, best_model, df, imputation_tracker):
    """
    Évaluation du modèle et imputation des valeurs manquantes
    """
    display(pd.DataFrame({'': ['=== PARTIE 5 : ÉVALUATION ET IMPUTATION DES VALEURS MANQUANTES ===']})
    .style.hide(axis='index'))
    
    try:
        # 1. État initial des données
        display(pd.DataFrame({'': ['=== 1. ÉTAT INITIAL DES DONNÉES ===']}).style.hide(axis='index'))
        
        initial_stats = pd.DataFrame({
            'Métrique': [
                'Nombre total d\'observations',
                'Nombre de valeurs manquantes (margin_low)',
                'Pourcentage de valeurs manquantes',
                'Nombre d\'observations train',
                'Nombre d\'observations test'
            ],
            'Valeur': [
                len(df),
                df['margin_low'].isna().sum(),
                f"{(df['margin_low'].isna().sum() / len(df) * 100):.2f}%",
                len(X_train),
                len(X_test)
            ]
        })
        display(initial_stats.style.hide(axis='index'))

        # 2. Validation croisée sur les données d'entraînement
        display(pd.DataFrame({'': ['=== 2. VALIDATION CROISÉE ===']}).style.hide(axis='index'))
        
        # Nettoyage des données pour la validation croisée
        mask_train = ~(X_train.isna().any(axis=1) | y_train.isna())
        X_train_clean = X_train[mask_train]
        y_train_clean = y_train[mask_train]
        
        if len(X_train_clean) > 0:
            cv = KFold(n_splits=min(10, len(X_train_clean)), shuffle=True, random_state=42)
            
            # Calcul des scores CV
            cv_scores = cross_val_score(best_model, X_train_clean, y_train_clean, cv=cv)
            cv_rmse = np.sqrt(-cross_val_score(best_model, X_train_clean, y_train_clean, 
            scoring='neg_mean_squared_error', cv=cv))
            cv_mae = -cross_val_score(best_model, X_train_clean, y_train_clean, 
            scoring='neg_mean_absolute_error', cv=cv)
            
            cv_results = pd.DataFrame({
                'Métrique': [
                    'R² CV (moyenne ± écart-type)',
                    'RMSE CV (moyenne ± écart-type)',
                    'MAE CV (moyenne ± écart-type)'
                ],
                'Valeur': [
                    f"{cv_scores.mean():.4f} (±{cv_scores.std()*2:.4f})",
                    f"{cv_rmse.mean():.4f} (±{cv_rmse.std()*2:.4f})",
                    f"{cv_mae.mean():.4f} (±{cv_mae.std()*2:.4f})"
                ]
            })
            display(cv_results.style.hide(axis='index'))
        
        # 3. Évaluation sur le jeu de test
        display(pd.DataFrame({'': ['=== 3. ÉVALUATION SUR LE JEU DE TEST ===']}).style.hide(axis='index'))
        
        # Nettoyage des données de test
        mask_test = ~(X_test.isna().any(axis=1) | y_test.isna())
        X_test_clean = X_test[mask_test]
        y_test_clean = y_test[mask_test]
        
        if len(X_test_clean) > 0:
            # Prédictions sur le test
            y_pred_test = best_model.predict(X_test_clean)
            
            # Calcul des métriques
            test_metrics = {
                'r2': r2_score(y_test_clean, y_pred_test),
                'rmse': np.sqrt(mean_squared_error(y_test_clean, y_pred_test)),
                'mae': mean_absolute_error(y_test_clean, y_pred_test),
                'explained_variance': explained_variance_score(y_test_clean, y_pred_test)
            }
				# Test d'hétéroscédasticité et Durbin-Watson pour la phase test
            residuals_test = y_test_clean - y_pred_test
            X_test_stats = sm.add_constant(X_test_clean)
            dw_test = durbin_watson(residuals_test)
            _, het_pvalue, _, _ = het_breuschpagan(residuals_test, X_test_stats)

			# Mise à jour des métriques de test
            test_metrics.update({
				'dw_test': dw_test,
				'het_pvalue': het_pvalue
			})
            # Enregistrement dans le tracker
            imputation_tracker.record_metrics('test', best_model, **test_metrics)
            
            test_results = pd.DataFrame({
                'Métrique': list(test_metrics.keys()),
                'Valeur': [f"{v:.4f}" for v in test_metrics.values()]
            })
            display(test_results.style.hide(axis='index'))
        
        # 4. Imputation des valeurs manquantes
        display(pd.DataFrame({'': ['=== 4. IMPUTATION DES VALEURS MANQUANTES ===']}).style.hide(axis='index'))
        
        # Identification des valeurs à imputer
        df_impute = df.copy()
        missing_mask = df_impute['margin_low'].isna()
        X_missing = df_impute[missing_mask].drop(['margin_low', 'margin_low_missing'], axis=1)
        
        if len(X_missing) > 0:
            # Imputation
            imputed_values = best_model.predict(X_missing)
            df_impute.loc[missing_mask, 'margin_low'] = imputed_values
            
            # Statistiques sur l'imputation
            imputation_stats = pd.DataFrame({
                'Métrique': [
                    'Nombre de valeurs imputées',
                    'Moyenne des valeurs imputées',
                    'Écart-type des valeurs imputées',
                    'Minimum imputé',
                    'Maximum imputé'
                ],
                'Valeur': [
                    len(imputed_values),
                    f"{np.mean(imputed_values):.4f}",
                    f"{np.std(imputed_values):.4f}",
                    f"{np.min(imputed_values):.4f}",
                    f"{np.max(imputed_values):.4f}"
                ]
            })
            display(imputation_stats.style.hide(axis='index'))
            
            # Test de Kolmogorov-Smirnov pour comparer les distributions
            original_values = df_impute[~missing_mask]['margin_low'].dropna()
            if len(original_values) > 0:
                ks_stat, ks_pvalue = stats.ks_2samp(original_values, imputed_values)
                
                ks_results = pd.DataFrame({
                    'Métrique': ['Statistique KS', 'P-value KS', 'Interprétation'],
                    'Valeur': [
                        f"{ks_stat:.4f}",
                        f"{ks_pvalue:.4e}",
                        'Distributions similaires' if ks_pvalue > 0.05 else 'Distributions différentes'
                    ]
                })
                display(ks_results.style.hide(axis='index'))
                
                # Visualisation des distributions
                plt.figure(figsize=(10, 6))
                sns.kdeplot(data=original_values, label='Valeurs existantes', alpha=0.5)
                sns.kdeplot(data=pd.Series(imputed_values), label='Valeurs imputées', alpha=0.5)
                plt.title('Comparaison des distributions')
                plt.xlabel('margin_low')
                plt.ylabel('Densité')
                plt.legend()
                plt.show()
        
        return df_impute
        
    except Exception as e:
        display(pd.DataFrame({'Erreur': [str(e)]}).style.hide(axis='index'))
        print(f"Erreur détaillée : {e}")
        raise
In [24]:
# === PARTIE 6 : ANALYSE POST-IMPUTATION ===

# Fonction utilitaire pour visualiser l'évolution des métriques
def calculate_prediction_intervals(model, X, y_pred, X_train, y_train, alpha=0.05):
    """
    Calcule les intervalles de confiance pour les prédictions
    """
    # Ajout d'une constante pour l'intercept
    X_const = sm.add_constant(X)
    X_train_const = sm.add_constant(X_train)

    # Calcul de la variance des résidus
    y_train_pred = model.predict(X_train)
    mse = mean_squared_error(y_train, y_train_pred)

    # Calcul de la matrice de variance-covariance
    var_covar_matrix = mse * np.linalg.inv(X_train_const.T.dot(X_train_const))

    # Calcul des erreurs standards
    std_errors = np.sqrt(np.diagonal(X_const.dot(var_covar_matrix).dot(X_const.T)))

    # Calcul des intervalles
    t_value = stats.t.ppf(1 - alpha/2, len(y_train) - X_train.shape[1])

    return pd.DataFrame({
        'prediction': y_pred,
        'lower_bound': y_pred - t_value * std_errors,
        'upper_bound': y_pred + t_value * std_errors,
        'std_error': std_errors
    })

def plot_metrics_evolution(metrics_evolution, phases_attendues, noms_phases):
    """
    Crée les 4 graphiques d'évolution des métriques clés à travers les phases
    
    Parameters:
        metrics_evolution (pd.DataFrame): DataFrame contenant l'historique des métriques
        phases_attendues (list): Liste des phases dans l'ordre chronologique
        noms_phases (list): Noms des phases pour l'affichage
    """
    fig, axes = plt.subplots(2, 2, figsize=(15, 12))
    
    # Configuration des métriques à tracer
    metrics_to_plot = [
        ('r2', 'R²', axes[0,0], {'min': 0, 'max': 1}),
        ('rmse', 'RMSE', axes[0,1], {}),
        ('mae', 'MAE', axes[1,0], {}),
        ('explained_variance', 'Variance expliquée', axes[1,1], {'min': 0, 'max': 1})
    ]
    
    for metric, title, ax, ylim in metrics_to_plot:
        if metric in metrics_evolution.columns:
            values = []
            for phase in phases_attendues:
                mask = metrics_evolution['phase'] == phase
                if any(mask):
                    values.append(metrics_evolution.loc[mask, metric].iloc[0])
                else:
                    values.append(None)
            
            # Tracer la ligne et les points
            x = range(len(noms_phases))
            valid_points = [(i, v) for i, v in enumerate(values) if v is not None]
            if valid_points:
                x_valid, y_valid = zip(*valid_points)
                ax.plot(x_valid, y_valid, 'o-', linewidth=2, markersize=8)
                
                # Ajouter les valeurs sur les points
                for i, value in zip(x_valid, y_valid):
                    ax.annotate(f'{value:.3f}', 
                                (i, value),
                                textcoords="offset points",
                                xytext=(0,10),
                                ha='center')
            
            # Configuration du graphique
            ax.set_title(f'Évolution du {title}')
            ax.set_xticks(x)
            ax.set_xticklabels(noms_phases, rotation=45)
            ax.grid(True)
            
            # Appliquer les limites si spécifiées
            if 'min' in ylim:
                ax.set_ylim(bottom=ylim['min'])
            if 'max' in ylim:
                ax.set_ylim(top=ylim['max'])
    
    plt.tight_layout()
    return fig

def display_final_summary(metrics_evolution, final_metrics, imputation_metrics):
    """
    Affiche le tableau récapitulatif final avec toutes les phases
    """
    phases_attendues = ['initial', 'with_outliers', 'without_outliers', 'test', 'final']
    noms_phases = ['Initial', 'Avec outliers', 'Sans outliers', 'Test', 'Final']
    
    summary = pd.DataFrame({
        'Phase': noms_phases,
        'R²': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'r2'].iloc[0] 
                if phase in metrics_evolution['phase'].values else None 
                for phase in phases_attendues],
        'RMSE': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'rmse'].iloc[0] 
                if phase in metrics_evolution['phase'].values else None 
                for phase in phases_attendues],
        'MAE': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'mae'].iloc[0] 
                if phase in metrics_evolution['phase'].values else None 
                for phase in phases_attendues],
        'Variance expliquée': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'explained_variance'].iloc[0] 
                                if phase in metrics_evolution['phase'].values else None 
                                for phase in phases_attendues],
        'Test DW': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'dw_test'].iloc[0] 
                    if phase in metrics_evolution['phase'].values else None 
                    for phase in phases_attendues],
        'P-value (hétéro)': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'het_pvalue'].iloc[0] 
                            if phase in metrics_evolution['phase'].values else None 
                            for phase in phases_attendues]
    })
    
    # Amélioration du format d'affichage
    display(pd.DataFrame({'': ['=== TABLEAU RÉCAPITULATIF DES PHASES ===']}).style.hide(axis='index'))
    display(summary.style.format({
        'R²': '{:.4f}',
        'RMSE': '{:.4f}',
        'MAE': '{:.4f}',
        'Variance expliquée': '{:.4f}',
        'Test DW': '{:.4f}',
        'P-value (hétéro)': '{:.2e}'
    }).hide(axis='index'))
    
    # Ajout d'une analyse des améliorations
    ameliorations = pd.DataFrame({
        'Métrique': ['R²', 'RMSE', 'MAE'],
        'Amélioration initiale': [
            f"{((summary['R²'].iloc[-1] - summary['R²'].iloc[0])/summary['R²'].iloc[0]*100):.1f}%",
            f"{((summary['RMSE'].iloc[-1] - summary['RMSE'].iloc[0])/summary['RMSE'].iloc[0]*100):.1f}%",
            f"{((summary['MAE'].iloc[-1] - summary['MAE'].iloc[0])/summary['MAE'].iloc[0]*100):.1f}%"
        ],
        'Amélioration après outliers': [
            f"{((summary['R²'].iloc[-1] - summary['R²'].iloc[2])/summary['R²'].iloc[2]*100):.1f}%",
            f"{((summary['RMSE'].iloc[-1] - summary['RMSE'].iloc[2])/summary['RMSE'].iloc[2]*100):.1f}%",
            f"{((summary['MAE'].iloc[-1] - summary['MAE'].iloc[2])/summary['MAE'].iloc[2]*100):.1f}%"
        ]
    })
    
    display(pd.DataFrame({'': ['=== ANALYSE DES AMÉLIORATIONS ===']}).style.hide(axis='index'))
    display(ameliorations.style.hide(axis='index'))
    
    return summary
In [25]:
# === PARTIE 7 : ANALYSE POST-IMPUTATION ET ÉVALUATION FINALE ===
def verify_regression_assumptions(X, y, model, title=""):
    """
    Vérifie les hypothèses de la régression linéaire
    
    Parameters:
        X (pd.DataFrame): Variables explicatives
        y (pd.Series): Variable cible
        model: Modèle de régression
        title (str): Titre pour les visualisations
        
    Returns:
        dict: Résultats des tests statistiques
    """
    # Préparation des données
    y_pred = model.predict(X)
    residuals = y - y_pred
    
    # 1. Test de Durbin-Watson (indépendance)
    dw_stat = durbin_watson(residuals)
    
    # 2. Test d'hétéroscédasticité (Breusch-Pagan)
    X_with_const = sm.add_constant(X)
    _, het_pvalue, _, _ = het_breuschpagan(residuals, X_with_const)
    
    # 3. Test de normalité (Shapiro-Wilk)
    _, shapiro_pvalue = stats.shapiro(residuals)
    
    # 4. Calcul des coefficients VIF
    vif_data = pd.DataFrame()
    vif_data["Variable"] = X.columns
    vif_data["VIF"] = [variance_inflation_factor(X.values, i) 
                       for i in range(X.shape[1])]
    
    # Visualisations
    fig, axes = plt.subplots(2, 2, figsize=(15, 10))
    
    # QQ Plot
    stats.probplot(residuals, dist="norm", plot=axes[0,0])
    axes[0,0].set_title("Q-Q Plot des résidus")
    
    # Résidus vs valeurs prédites
    axes[0,1].scatter(y_pred, residuals, alpha=0.5)
    axes[0,1].axhline(y=0, color='r', linestyle='--')
    axes[0,1].set_xlabel("Valeurs prédites")
    axes[0,1].set_ylabel("Résidus")
    axes[0,1].set_title("Résidus vs Valeurs prédites")
    
    # Distribution des résidus
    sns.histplot(residuals, kde=True, ax=axes[1,0])
    axes[1,0].set_title("Distribution des résidus")
    
    # Autocorrélation
    sm.graphics.tsa.plot_acf(residuals, lags=40, alpha=0.05, ax=axes[1,1])
    axes[1,1].set_title("Autocorrélation des résidus")
    
    plt.suptitle(f"Diagnostics de régression - {title}", y=1.02)
    plt.tight_layout()
    plt.show()
    
    return {
        'residuals': residuals,
        'y_pred': y_pred,
        'dw_stat': dw_stat,
        'het_pvalue': het_pvalue,
        'shapiro_pvalue': shapiro_pvalue,
        'vif_data': vif_data
    }

def partie_6(df_imputed, X_train, y_train, best_model, imputation_tracker):
    """
    Analyse post-imputation et évaluation finale du modèle
    """
    display(pd.DataFrame({'': ['=== PARTIE 6 : ANALYSE POST-IMPUTATION ===']})
            .style.hide(axis='index'))
    
    try:
        # 1. Vérification des hypothèses sur données imputées
        display(pd.DataFrame({'': ['=== 1. VÉRIFICATION DES HYPOTHÈSES POST-IMPUTATION ===']})
                .style.hide(axis='index'))
        
        X = df_imputed.drop(['margin_low', 'margin_low_missing'], axis=1)
        y = df_imputed['margin_low']
        
        # Vérification complète des hypothèses
        assumption_results = verify_regression_assumptions(X, y, best_model, "Post-imputation")
        
        # 2. Analyse des outliers post-imputation
        display(pd.DataFrame({'': ['=== 2. ANALYSE DES OUTLIERS POST-IMPUTATION ===']})
                .style.hide(axis='index'))
        
        # Calcul des scores d'outliers
        residuals = assumption_results['residuals']
        y_pred = assumption_results['y_pred']
        z_scores = np.abs(stats.zscore(residuals))
        mad_scores = np.abs(residuals - np.median(residuals)) / stats.median_abs_deviation(residuals)
        
        # Visualisation des outliers
        plt.figure(figsize=(12, 7))
        plt.scatter(y_pred, residuals, alpha=0.5, label='Points normaux')
        plt.scatter(y_pred[z_scores > 3], residuals[z_scores > 3], 
                    color='red', label='Outliers (Z-score > 3)', alpha=0.7)
        plt.scatter(y_pred[mad_scores > 3.5], residuals[mad_scores > 3.5], 
                    color='orange', label='Outliers (MAD > 3.5)', alpha=0.7)
        plt.axhline(y=0, color='r', linestyle='--')
        plt.xlabel('Valeurs prédites')
        plt.ylabel('Résidus')
        plt.title('Graphique des résidus avec outliers potentiels')
        plt.legend()
        plt.show()
        
        # 3. Métriques finales enrichies
        display(pd.DataFrame({'': ['=== 3. MÉTRIQUES FINALES ===']})
                .style.hide(axis='index'))
        
        final_metrics = {
            'phase': 'final',
            'r2': r2_score(y, y_pred),
            'rmse': np.sqrt(mean_squared_error(y, y_pred)),
            'mae': mean_absolute_error(y, y_pred),
            'explained_variance': explained_variance_score(y, y_pred),
            'dw_test': assumption_results['dw_stat'],
            'het_pvalue': assumption_results['het_pvalue'],
            'shapiro_pvalue': assumption_results['shapiro_pvalue'],
            'mad_score_mean': np.mean(mad_scores),
            'z_score_mean': np.mean(z_scores),
            'residuals': residuals  # Ajout pour la partie finale
        }
        
        # Enregistrement dans le tracker
        imputation_tracker.record_metrics('final', best_model, **final_metrics)
        
        # 4. Visualisation finale et comparaisons
        display(pd.DataFrame({'': ['=== 4. VISUALISATION FINALE ET COMPARAISONS ===']})
                .style.hide(axis='index'))
        
        # Extraction de l'historique des métriques
        metrics_evolution = pd.DataFrame(imputation_tracker.metrics_history)
        phases_attendues = ['initial', 'with_outliers', 'without_outliers', 'test', 'final']
        noms_phases = ['Initial', 'Avec outliers', 'Sans outliers', 'Test', 'Final']
        
        # Création des graphiques d'évolution
        fig = plot_metrics_evolution(metrics_evolution, phases_attendues, noms_phases)
        plt.show()
        
        # Affichage du tableau récapitulatif
        summary = display_final_summary(metrics_evolution, final_metrics, None)
        
        # 5. Analyse comparative finale
        display(pd.DataFrame({'': ['=== 5. ANALYSE COMPARATIVE FINALE ===']})
                .style.hide(axis='index'))
        
        comparaison_finale = pd.DataFrame({
            'Aspect': [
                'Performance globale',
                'Stabilité du modèle',
                'Robustesse aux outliers',
                'Respect des hypothèses',
                'Impact sur les données'
            ],
            'Évaluation': [
                f"R² final = {final_metrics['r2']:.4f}, RMSE final = {final_metrics['rmse']:.4f}",
                f"MAE = {final_metrics['mae']:.4f}",
                f"Outliers détectés = {(np.sum(z_scores > 3)/len(z_scores))*100:.1f}%",
                f"DW = {final_metrics['dw_test']:.2f}, Het. p-value = {final_metrics['het_pvalue']:.2e}",
                f"Var. exp. = {final_metrics['explained_variance']:.4f}"
            ],
            'Interprétation': [
                'Satisfaisant' if final_metrics['r2'] > 0.7 else 'À améliorer',
                'Stable' if final_metrics['mae'] < 0.3 else 'Instable',
                'Acceptable' if (np.sum(z_scores > 3)/len(z_scores)) < 0.05 else 'Problématique',
                'Validé' if final_metrics['dw_test'] > 1.5 and final_metrics['het_pvalue'] > 0.05 else 'Non validé',
                'Bon' if final_metrics['explained_variance'] > 0.7 else 'À améliorer'
            ]
        })
        
        display(comparaison_finale.style.hide(axis='index'))
        
        return final_metrics

    except Exception as e:
        display(pd.DataFrame({'Erreur': [str(e)]}).style.hide(axis='index'))
        print(f"Erreur détaillée : {e}")
        raise
In [26]:
def partie_6(df_imputed, X_train, y_train, best_model, imputation_tracker):
    """
    Analyse post-imputation et évaluation finale du modèle
    """
    display(pd.DataFrame({'': ['=== PARTIE 6 : ANALYSE POST-IMPUTATION ===']})
            .style.hide(axis='index'))
    
    try:
        # 1. Vérification des hypothèses sur données imputées
        display(pd.DataFrame({'': ['=== 1. VÉRIFICATION DES HYPOTHÈSES POST-IMPUTATION ===']})
                .style.hide(axis='index'))
        
        X = df_imputed.drop(['margin_low', 'margin_low_missing'], axis=1)
        y = df_imputed['margin_low']
        
        # Vérification complète des hypothèses
        assumption_results = verify_regression_assumptions(X, y, best_model, "Post-imputation")
        
        # 2. Analyse des outliers post-imputation
        display(pd.DataFrame({'': ['=== 2. ANALYSE DES OUTLIERS POST-IMPUTATION ===']})
                .style.hide(axis='index'))
        
        # Calcul des scores d'outliers
        residuals = assumption_results['residuals']
        y_pred = assumption_results['y_pred']
        z_scores = np.abs(stats.zscore(residuals))
        mad_scores = np.abs(residuals - np.median(residuals)) / stats.median_abs_deviation(residuals)
        
        # Visualisation des outliers
        plt.figure(figsize=(12, 7))
        plt.scatter(y_pred, residuals, alpha=0.5, label='Points normaux')
        plt.scatter(y_pred[z_scores > 3], residuals[z_scores > 3], 
                   color='red', label='Outliers (Z-score > 3)', alpha=0.7)
        plt.scatter(y_pred[mad_scores > 3.5], residuals[mad_scores > 3.5], 
                   color='orange', label='Outliers (MAD > 3.5)', alpha=0.7)
        plt.axhline(y=0, color='r', linestyle='--')
        plt.xlabel('Valeurs prédites')
        plt.ylabel('Résidus')
        plt.title('Graphique des résidus avec outliers potentiels')
        plt.legend()
        plt.show()
        
        # 3. Métriques finales enrichies
        display(pd.DataFrame({'': ['=== 3. MÉTRIQUES FINALES ===']})
                .style.hide(axis='index'))
        
        final_metrics = {
            'r2': r2_score(y, y_pred),
            'rmse': np.sqrt(mean_squared_error(y, y_pred)),
            'mae': mean_absolute_error(y, y_pred),
            'explained_variance': explained_variance_score(y, y_pred),
            'dw_test': assumption_results['dw_stat'],
            'het_pvalue': assumption_results['het_pvalue'],
            'shapiro_pvalue': assumption_results['shapiro_pvalue'],
            'mad_score_mean': np.mean(mad_scores),
            'z_score_mean': np.mean(z_scores),
            'residuals': residuals
        }
        
        # Enregistrement dans le tracker
        imputation_tracker.record_metrics(phase='final', model=best_model, **final_metrics)
        
        # 4. Visualisation finale et comparaisons
        display(pd.DataFrame({'': ['=== 4. VISUALISATION FINALE ET COMPARAISONS ===']})
                .style.hide(axis='index'))
        
        # Extraction et combinaison des métriques
        metrics_evolution = pd.DataFrame(imputation_tracker.metrics_history)
        metrics_from_tracker = pd.DataFrame(tracker.metrics_history)
        phases_initiales = metrics_from_tracker[metrics_from_tracker['phase'].isin(['initial', 'with_outliers', 'without_outliers'])]
        all_metrics = pd.concat([phases_initiales, metrics_evolution])
        
        # Configuration des phases pour les graphiques
        phases_attendues = ['initial', 'with_outliers', 'without_outliers', 'test', 'final']
        noms_phases = ['Initial', 'Avec outliers', 'Sans outliers', 'Test', 'Final']
        
        # Création des graphiques d'évolution
        fig = plot_metrics_evolution(all_metrics, phases_attendues, noms_phases)
        plt.show()
        
        # Affichage du tableau récapitulatif
        summary = display_final_summary(all_metrics, final_metrics, None)
        
        # 5. Analyse comparative finale
        display(pd.DataFrame({'': ['=== 5. ANALYSE COMPARATIVE FINALE ===']})
                .style.hide(axis='index'))
        
        comparaison_finale = pd.DataFrame({
            'Aspect': [
                'Performance globale',
                'Stabilité du modèle',
                'Robustesse aux outliers',
                'Respect des hypothèses',
                'Impact sur les données'
            ],
            'Évaluation': [
                f"R² final = {final_metrics['r2']:.4f}, RMSE final = {final_metrics['rmse']:.4f}",
                f"MAE = {final_metrics['mae']:.4f}",
                f"Outliers détectés = {(np.sum(z_scores > 3)/len(z_scores))*100:.1f}%",
                f"DW = {final_metrics['dw_test']:.2f}, Het. p-value = {final_metrics['het_pvalue']:.2e}",
                f"Var. exp. = {final_metrics['explained_variance']:.4f}"
            ],
            'Interprétation': [
                'Satisfaisant' if final_metrics['r2'] > 0.7 else 'À améliorer',
                'Stable' if final_metrics['mae'] < 0.3 else 'Instable',
                'Acceptable' if (np.sum(z_scores > 3)/len(z_scores)) < 0.05 else 'Problématique',
                'Validé' if final_metrics['dw_test'] > 1.5 and final_metrics['het_pvalue'] > 0.05 else 'Non validé',
                'Bon' if final_metrics['explained_variance'] > 0.7 else 'À améliorer'
            ]
        })
        
        display(comparaison_finale.style.hide(axis='index'))
        
        return final_metrics
        
    except Exception as e:
        display(pd.DataFrame({'Erreur': [str(e)]}).style.hide(axis='index'))
        print(f"Erreur détaillée : {e}")
        raise
In [27]:
def execute_analysis(X_train, X_test, y_train, y_test, best_model, df, imputation_tracker):
    """
    Fonction principale d'exécution de l'analyse complète
    """
    try:
        print("\n=== DÉMARRAGE DE L'ANALYSE ===")
        
        # Vérification des données initiales
        if not all(isinstance(x, (pd.DataFrame, pd.Series)) for x in [X_train, X_test, y_train, y_test]):
            raise ValueError("Les données d'entrée doivent être des DataFrames ou Series pandas")
            
        # Configuration des visualisations simplifiée
        sns.set_theme()
        
        print("\n=== EXÉCUTION DE LA PARTIE 5 ===")
        df_imputed = partie_5(X_train, X_test, y_train, y_test, best_model, df, imputation_tracker)
        
        if df_imputed is None:
            raise ValueError("Échec de l'imputation dans la partie 5")
            
        print("\n=== EXÉCUTION DE LA PARTIE 6 ===")
        final_metrics = partie_6(df_imputed, X_train, y_train, best_model, imputation_tracker)
        
        # Résumé final des résultats
        print("\n=== RÉSUMÉ FINAL ===")
        resume = pd.DataFrame({
            'Métrique': [
                'R² final',
                'RMSE final',
                'MAE final',
                'Variance expliquée',
                'Test Durbin-Watson',
                'P-value hétéroscédasticité',
                'Outliers détectés'
            ],
            'Valeur': [
                f"{final_metrics['r2']:.4f}",
                f"{final_metrics['rmse']:.4f}",
                f"{final_metrics['mae']:.4f}",
                f"{final_metrics['explained_variance']:.4f}",
                f"{final_metrics['dw_test']:.4f}",
                f"{final_metrics['het_pvalue']:.2e}",
                f"{(np.sum(np.abs(stats.zscore(final_metrics['residuals'])) > 3)/len(final_metrics['residuals'])*100):.1f}%"
            ]
        })
        display(resume.style.hide(axis='index'))
        
        # Sauvegarde optionnelle des résultats
        save = input("\nVoulez-vous sauvegarder les résultats? (o/n): ").lower()
        if save == 'o':
            timestamp = pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')
            filename = f'resultats_analyse_{timestamp}.csv'
            df_imputed.to_csv(filename, index=False)
            print(f"\nRésultats sauvegardés dans '{filename}'")
        
        return df_imputed, final_metrics
        
    except Exception as e:
        print(f"\nErreur lors de l'exécution : {str(e)}")
        print("\nTraceback complet :")
        traceback.print_exc()
        return None, None
In [28]:
def display_final_summary(metrics_evolution, final_metrics, imputation_metrics):
    """
    Affiche un résumé organisé et complet des résultats
    """
    if not isinstance(metrics_evolution, pd.DataFrame):
        metrics_evolution = pd.DataFrame(metrics_evolution)

    # Ne garder qu'une seule instance de chaque phase
    metrics_evolution = metrics_evolution.drop_duplicates(subset=['phase'], keep='last')
    
    # 1. Évolution des métriques par phase
    display(pd.DataFrame({'': ['=== ÉVOLUTION DES MÉTRIQUES PAR PHASE ===']}).style.hide(axis='index'))
    
    # Préparer les données pour l'évolution
    phases_data = []
    phase_order = ['initial', 'with_outliers', 'without_outliers', 'test', 'final']
    phase_names = ['Initial', 'Avec outliers', 'Sans outliers', 'Test', 'Final']

    summary = pd.DataFrame()
    for phase, phase_name in zip(phase_order, phase_names):
        if phase in metrics_evolution['phase'].values:
            phase_data = metrics_evolution[metrics_evolution['phase'] == phase].iloc[0]
            metrics = {
                'R²': phase_data['r2'] if not pd.isna(phase_data.get('r2')) else None,
                'RMSE': phase_data['rmse'] if not pd.isna(phase_data.get('rmse')) else None,
                'MAE': phase_data['mae'] if not pd.isna(phase_data.get('mae')) else None,
                'Variance expliquée': phase_data['explained_variance'] if not pd.isna(phase_data.get('explained_variance')) else None,
                'Test DW': phase_data.get('dw_test'),
                'P-value (hétéro)': phase_data.get('het_pvalue')
            }
            summary = pd.concat([summary, pd.DataFrame(metrics, index=[phase_name])])

    display(summary.style.format({
        'R²': '{:.4f}',
        'RMSE': '{:.4f}',
        'MAE': '{:.4f}',
        'Variance expliquée': '{:.4f}',
        'Test DW': '{:.4f}',
        'P-value (hétéro)': '{:.2e}'
    }, na_rep='N/A').hide(axis='index'))
    
    # 2. Analyse des améliorations
    if 'test' in metrics_evolution['phase'].values and 'final' in metrics_evolution['phase'].values:
        display(pd.DataFrame({'': ['=== ANALYSE DES AMÉLIORATIONS ===']}).style.hide(axis='index'))
        
        test_data = metrics_evolution[metrics_evolution['phase'] == 'test'].iloc[0]
        final_data = metrics_evolution[metrics_evolution['phase'] == 'final'].iloc[0]
        
        metrics_to_compare = ['R²', 'RMSE', 'MAE']
        data_keys = ['r2', 'rmse', 'mae']
        
        ameliorations = pd.DataFrame({
            'Métrique': metrics_to_compare,
            'Valeur Test': [f"{test_data[key]:.4f}" for key in data_keys],
            'Valeur Finale': [f"{final_data[key]:.4f}" for key in data_keys],
            'Évolution': [
                f"{((final_data[key] - test_data[key])/abs(test_data[key])*100):.1f}%"
                for key in data_keys
            ]
        })
        display(ameliorations.style.hide(axis='index'))
    
    # 3. Résumé final complet
    display(pd.DataFrame({'': ['=== RÉSUMÉ FINAL DÉTAILLÉ ===']}).style.hide(axis='index'))
    
    final_data = metrics_evolution[metrics_evolution['phase'] == 'final'].iloc[0]
    
    resume_final = pd.DataFrame({
        'Métrique': [
            'Métriques de performance',
            '└─ R²',
            '└─ RMSE',
            '└─ MAE',
            '└─ Variance expliquée',
            'Tests statistiques',
            '└─ Test Durbin-Watson',
            '└─ P-value hétéroscédasticité',
            '└─ P-value Shapiro-Wilk',
            'Métriques de validation croisée',
            '└─ R² CV (moyenne ± écart-type)',
            '└─ RMSE CV (moyenne ± écart-type)',
            'Scores de dispersion',
            '└─ Score MAD moyen',
            '└─ Score Z moyen',
            'Autres métriques',
            '└─ Erreur maximale',
            '└─ Intervalle de confiance'
        ],
        'Valeur': [
            '',
            f"{final_data['r2']:.4f}",
            f"{final_data['rmse']:.4f}",
            f"{final_data['mae']:.4f}",
            f"{final_data['explained_variance']:.4f}",
            '',
            f"{final_data['dw_test']:.4f}" if pd.notnull(final_data.get('dw_test')) else 'N/A',
            f"{final_data['het_pvalue']:.2e}" if pd.notnull(final_data.get('het_pvalue')) else 'N/A',
            f"{final_data['shapiro_pvalue']:.2e}" if pd.notnull(final_data.get('shapiro_pvalue')) else 'N/A',
            '',
            f"{final_data['r2_cv_mean']:.4f} ± {final_data['r2_cv_std']:.4f}" if pd.notnull(final_data.get('r2_cv_mean')) else 'N/A',
            f"{final_data['rmse_cv_mean']:.4f} ± {final_data['rmse_cv_std']:.4f}" if pd.notnull(final_data.get('rmse_cv_mean')) else 'N/A',
            '',
            f"{final_data['mad_score_mean']:.4f}" if pd.notnull(final_data.get('mad_score_mean')) else 'N/A',
            f"{final_data['z_score_mean']:.4f}" if pd.notnull(final_data.get('z_score_mean')) else 'N/A',
            '',
            f"{final_data['max_error']:.4f}" if pd.notnull(final_data.get('max_error')) else 'N/A',
            f"{final_data['interval_coverage']:.2%}" if pd.notnull(final_data.get('interval_coverage')) else 'N/A'
        ]
    })
    display(resume_final.style.hide(axis='index'))
    
    return summary
In [29]:
df_final, metrics_final = execute_analysis(X_train, X_test, y_train, y_test, best_model, df, imputation_tracker)
=== DÉMARRAGE DE L'ANALYSE ===

=== EXÉCUTION DE LA PARTIE 5 ===
=== PARTIE 5 : ÉVALUATION ET IMPUTATION DES VALEURS MANQUANTES ===
=== 1. ÉTAT INITIAL DES DONNÉES ===
Métrique Valeur
Nombre total d'observations 1500
Nombre de valeurs manquantes (margin_low) 0
Pourcentage de valeurs manquantes 0.00%
Nombre d'observations train 1350
Nombre d'observations test 150
=== 2. VALIDATION CROISÉE ===
Métrique Valeur
R² CV (moyenne ± écart-type) 0.6075 (±0.0618)
RMSE CV (moyenne ± écart-type) 0.4147 (±0.0450)
MAE CV (moyenne ± écart-type) 0.3192 (±0.0324)
=== 3. ÉVALUATION SUR LE JEU DE TEST ===
Métrique Valeur
r2 0.5974
rmse 0.3919
mae 0.3056
explained_variance 0.6095
dw_test 2.1752
het_pvalue 0.1453
=== 4. IMPUTATION DES VALEURS MANQUANTES ===
=== EXÉCUTION DE LA PARTIE 6 ===
=== PARTIE 6 : ANALYSE POST-IMPUTATION ===
=== 1. VÉRIFICATION DES HYPOTHÈSES POST-IMPUTATION ===
No description has been provided for this image
=== 2. ANALYSE DES OUTLIERS POST-IMPUTATION ===
No description has been provided for this image
=== 3. MÉTRIQUES FINALES ===
=== 4. VISUALISATION FINALE ET COMPARAISONS ===
No description has been provided for this image
=== ÉVOLUTION DES MÉTRIQUES PAR PHASE ===
R² RMSE MAE Variance expliquée Test DW P-value (hétéro)
0.6169 0.4107 0.3158 0.6169 2.0385 3.46e-33
0.6180 0.4129 0.3174 0.6180 N/A N/A
0.6335 0.3927 0.3074 0.6335 N/A N/A
0.5974 0.3919 0.3056 0.6095 2.1752 1.45e-01
0.6213 0.4059 0.3083 0.6213 2.0409 9.27e-34
=== ANALYSE DES AMÉLIORATIONS ===
Métrique Valeur Test Valeur Finale Évolution
R² 0.5974 0.6213 4.0%
RMSE 0.3919 0.4059 3.6%
MAE 0.3056 0.3083 0.9%
=== RÉSUMÉ FINAL DÉTAILLÉ ===
Métrique Valeur
Métriques de performance
└─ R² 0.6213
└─ RMSE 0.4059
└─ MAE 0.3083
└─ Variance expliquée 0.6213
Tests statistiques
└─ Test Durbin-Watson 2.0409
└─ P-value hétéroscédasticité 9.27e-34
└─ P-value Shapiro-Wilk 3.32e-07
Métriques de validation croisée
└─ R² CV (moyenne ± écart-type) N/A
└─ RMSE CV (moyenne ± écart-type) N/A
Scores de dispersion
└─ Score MAD moyen 1.2453
└─ Score Z moyen 0.7597
Autres métriques
└─ Erreur maximale N/A
└─ Intervalle de confiance N/A
=== 5. ANALYSE COMPARATIVE FINALE ===
Aspect Évaluation Interprétation
Performance globale R² final = 0.6213, RMSE final = 0.4059 À améliorer
Stabilité du modèle MAE = 0.3083 Instable
Robustesse aux outliers Outliers détectés = 0.9% Acceptable
Respect des hypothèses DW = 2.04, Het. p-value = 9.27e-34 Non validé
Impact sur les données Var. exp. = 0.6213 À améliorer
=== RÉSUMÉ FINAL ===
Métrique Valeur
R² final 0.6213
RMSE final 0.4059
MAE final 0.3083
Variance expliquée 0.6213
Test Durbin-Watson 2.0409
P-value hétéroscédasticité 9.27e-34
Outliers détectés 0.9%
In [30]:
# 5 - Analyse post-imputation et évaluation finale

# Configuration des paramètres de visualisation
sns.set_theme(style="whitegrid")
plt.rcParams['figure.figsize'] = (12, 6)

# === Titre principal ===
display(pd.DataFrame({'': ['=== 5 - ANALYSE POST-IMPUTATION ET ÉVALUATION FINALE ===']}).style.hide(axis='index'))

# Vérification des colonnes disponibles
print("\nColonnes disponibles dans le DataFrame:")
print(df.columns.tolist())

# === 1. Analyse des distributions ===
display(pd.DataFrame({'': ['=== 1. ANALYSE DES DISTRIBUTIONS ===']}).style.hide(axis='index'))

# Identification des valeurs originales et imputées
mask_missing = df['margin_low_missing'] == 1
values_imputed = df.loc[mask_missing, 'margin_low'].dropna()
values_original = df.loc[~mask_missing, 'margin_low'].dropna()

print(f"Nombre de valeurs imputées: {len(values_imputed)}")
print(f"Nombre de valeurs originales: {len(values_original)}")

if len(values_imputed) > 0 and len(values_original) > 0:
    # Comparaison des distributions
    plt.figure(figsize=(12, 6))
    sns.kdeplot(data=values_imputed, label='Valeurs imputées', color='red')
    sns.kdeplot(data=values_original, label='Valeurs existantes', color='blue')
    plt.title('Distribution des valeurs imputées vs. existantes')
    plt.xlabel('margin_low')
    plt.ylabel('Densité')
    plt.legend()
    plt.show()

    # Test de Kolmogorov-Smirnov
    ks_statistic, ks_pvalue = stats.ks_2samp(values_imputed, values_original)
    ks_results = pd.DataFrame([
        {'Test': 'Kolmogorov-Smirnov',
            'Statistique': f"{ks_statistic:.3f}",
            'P-valeur': f"{ks_pvalue:.2e}",
            'Interprétation': 'Distributions similaires' if ks_pvalue > 0.05 else 'Distributions différentes'}
        ])
    display(ks_results.style.hide(axis='index'))

# === 2. Vérification de la linéarité ===
display(pd.DataFrame({'': ['=== 2. VÉRIFICATION DE LA LINÉARITÉ ===']}).style.hide(axis='index'))

# Ajustement des features selon les colonnes disponibles
features = [col for col in ['Longueur', 'Hauteur', 'Diagonale', 'Marge_sup'] if col in df.columns]

if not features:
    print("Attention: Aucune colonne attendue n'a été trouvée dans le DataFrame")
else:
    # Conversion et préparation des données
    X_stats = df[features].astype(float)
    y_stats = df['margin_low'].astype(float)

    # Nettoyage des valeurs manquantes
    mask_stats = ~(X_stats.isna().any(axis=1) | y_stats.isna())
    X_stats_clean = X_stats[mask_stats]
    y_stats_clean = y_stats[mask_stats]

    for col in X_stats_clean.columns:
        plt.figure(figsize=(12, 4))
        
        # Relation avec la variable cible
        plt.subplot(121)
        sns.regplot(x=X_stats_clean[col], y=y_stats_clean, scatter_kws={'alpha':0.5}, line_kws={'color': 'red'})
        plt.title(f'Relation {col} vs margin_low')
        
        # Distribution des valeurs
        plt.subplot(122)
        sns.boxplot(x=X_stats_clean[col])
        plt.title(f'Distribution de {col}')
        
        # Calcul de la corrélation
        corr = np.corrcoef(X_stats_clean[col], y_stats_clean)[0,1]
        plt.suptitle(f'Analyse de {col} (correlation: {corr:.3f})')
        plt.tight_layout()
        plt.show()

    # === 3. Tests statistiques ===
    display(pd.DataFrame({'': ['=== 3. TESTS STATISTIQUES ===']}).style.hide(axis='index'))

    # Construction du modèle final
    model_final = sm.OLS(y_stats_clean, sm.add_constant(X_stats_clean)).fit()
    residuals = model_final.resid
    fitted_values = model_final.fittedvalues

    # Tests statistiques
    dw_stat = durbin_watson(residuals)
    _, het_pvalue, _, _ = het_breuschpagan(residuals, model_final.model.exog)
    shapiro_stat, shapiro_pvalue = stats.shapiro(residuals)

    # VIF
    vif_data = pd.DataFrame({
        'Variable': features,
        'VIF': [variance_inflation_factor(X_stats_clean.values, i) for i in range(X_stats_clean.shape[1])],
    })
    vif_data['Interprétation'] = pd.cut(
        vif_data['VIF'],
        bins=[-np.inf, 2, 5, 10, np.inf],
        labels=['Faible', 'Modérée', 'Élevée', 'Très élevée']
    )

    # Affichage des résultats des tests
    test_results = pd.DataFrame([
        {'Test': 'Durbin-Watson', 
            'Statistique': f"{dw_stat:.3f}",
            'P-valeur': '-',
            'Interprétation': 'Indépendant' if 1.5 < dw_stat < 2.5 else 'Dépendant'},
        {'Test': 'Breusch-Pagan', 
            'Statistique': '-',
            'P-valeur': f"{het_pvalue:.2e}",
            'Interprétation': 'Homoscédastique' if het_pvalue > 0.05 else 'Hétéroscédastique'},
        {'Test': 'Shapiro-Wilk', 
            'Statistique': f"{shapiro_stat:.3f}",
            'P-valeur': f"{shapiro_pvalue:.2e}",
            'Interprétation': 'Normal' if shapiro_pvalue > 0.05 else 'Non normal'}
    ])
    display(test_results.style.hide(axis='index'))
    display(vif_data.style.hide(axis='index'))

    # === 4. Analyse des résidus ===
    display(pd.DataFrame({'': ['=== 4. ANALYSE DES RÉSIDUS ===']}).style.hide(axis='index'))

    fig, axes = plt.subplots(2, 2, figsize=(15, 15))

    # QQ Plot
    sm.qqplot(residuals, line='45', ax=axes[0, 0])
    axes[0, 0].set_title('QQ Plot des résidus')

    # Résidus vs valeurs prédites
    axes[0, 1].scatter(fitted_values, residuals, alpha=0.5)
    axes[0, 1].axhline(y=0, color='r', linestyle='--')
    axes[0, 1].set_xlabel('Valeurs prédites')
    axes[0, 1].set_ylabel('Résidus')
    axes[0, 1].set_title('Résidus vs Valeurs prédites')

    # Distribution des résidus
    sns.histplot(residuals, kde=True, ax=axes[1, 0])
    axes[1, 0].set_title('Distribution des résidus')

    # Scale-Location plot
    axes[1, 1].scatter(fitted_values, np.sqrt(np.abs(residuals)), alpha=0.5)
    axes[1, 1].set_xlabel('Valeurs prédites')
    axes[1, 1].set_ylabel('√|Résidus standardisés|')
    axes[1, 1].set_title('Scale-Location Plot')

    plt.tight_layout()
    plt.show()

    # === 5. Analyse des outliers ===
    display(pd.DataFrame({'': ['=== 5. ANALYSE DES OUTLIERS ===']}).style.hide(axis='index'))

    # Calcul des scores
    z_scores = np.abs(stats.zscore(residuals))
    mad_scores = np.abs(residuals - np.median(residuals)) / stats.median_abs_deviation(residuals)

    # Identification des outliers
    outliers_zscore = np.where(z_scores > 3)[0]
    outliers_mad = np.where(mad_scores > 3.5)[0]

    # Résultats des outliers
    outliers_results = pd.DataFrame([
        {'Méthode': 'Z-score > 3', 'Nombre d\'outliers': len(outliers_zscore)},
        {'Méthode': 'MAD > 3.5', 'Nombre d\'outliers': len(outliers_mad)}
    ])
    display(outliers_results.style.hide(axis='index'))

    # Visualisation des outliers
    plt.figure(figsize=(12, 7))
    plt.scatter(fitted_values, residuals, alpha=0.5, label='Points normaux')
    plt.scatter(fitted_values[outliers_zscore], residuals[outliers_zscore], 
                color='red', label='Outliers (Z-score > 3)', alpha=0.7)
    plt.scatter(fitted_values[outliers_mad], residuals[outliers_mad], 
                color='orange', label='Outliers (MAD > 3.5)', alpha=0.7)
    plt.axhline(y=0, color='r', linestyle='--')
    plt.xlabel('Valeurs prédites')
    plt.ylabel('Résidus')
    plt.title('Graphique des résidus avec outliers potentiels')
    plt.legend()
    plt.show()

    # === 6. Métriques finales ===
    display(pd.DataFrame({'': ['=== 6. MÉTRIQUES FINALES ===']}).style.hide(axis='index'))

    # Calcul des métriques finales
    final_metrics = {
        'r2': model_final.rsquared,
        'rmse': np.sqrt(np.mean(residuals**2)),
        'mae': np.mean(np.abs(residuals)),
        'explained_variance': model_final.rsquared_adj,
        'max_error': np.max(np.abs(residuals)),
        'dw_test': dw_stat,
        'het_pvalue': het_pvalue,
        'shapiro_pvalue': shapiro_pvalue,
        'mad_score_mean': np.mean(mad_scores),
        'z_score_mean': np.mean(z_scores)
    }

    # Enregistrement des métriques finales
    imputation_tracker.record_metrics('final', model_final, **final_metrics)

    # === 7. Évolution des métriques ===
    display(pd.DataFrame({'': ['=== 7. ÉVOLUTION DES MÉTRIQUES ===']}).style.hide(axis='index'))

    # Extraction des métriques
    metrics_evolution = pd.DataFrame(imputation_tracker.metrics_history)
    phases_attendues = ['initial', 'with_outliers', 'without_outliers', 'final']
    noms_phases = ['Initialisation', 'Avec outliers', 'Sans outliers', 'Final']

    # Création du tableau de suivi
    metrics_suivi = pd.DataFrame({
        'Phase': noms_phases,
        'R²': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'r2'].values[0] 
                if phase in metrics_evolution['phase'].values else None 
                for phase in phases_attendues],
        'RMSE': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'rmse'].values[0] 
                if phase in metrics_evolution['phase'].values else None 
                for phase in phases_attendues],
        'MAE': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'mae'].values[0] 
                if phase in metrics_evolution['phase'].values else None 
                for phase in phases_attendues],
        'R² CV': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'r2_cv_mean'].values[0] 
                if phase in metrics_evolution['phase'].values else None 
                for phase in phases_attendues],
        'Durbin-Watson': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'dw_test'].values[0] 
                            if phase in metrics_evolution['phase'].values else None 
                            for phase in phases_attendues],
        'P-value (hétéro)': [metrics_evolution.loc[metrics_evolution['phase'] == phase, 'het_pvalue'].values[0] 
                            if phase in metrics_evolution['phase'].values else None 
                            for phase in phases_attendues]
    })

    # Affichage du tableau formaté
    metrics_suivi_styled = metrics_suivi.style\
        .format({
            'R²': '{:.3f}',
            'RMSE': '{:.3f}',
            'MAE': '{:.3f}',
            'R² CV': '{:.3f}',
            'Durbin-Watson': '{:.3f}',
            'P-value (hétéro)': '{:.2e}'
        })\
        .hide(axis='index')

    display(metrics_suivi_styled)

    # Visualisation de l'évolution des métriques
    fig, axes = plt.subplots(2, 2, figsize=(15, 12))
    metrics_to_plot = [
        ('r2', 'R²', 0, 1), 
        ('rmse', 'RMSE', None, None),
        ('mae', 'MAE', None, None), 
        ('r2_cv_mean', 'R² CV', 0, 1)
    ]

    for (metric, title, ymin, ymax), ax in zip(metrics_to_plot, axes.ravel()):
        if metric in metrics_evolution.columns:
            values = [metrics_evolution.loc[metrics_evolution['phase'] == phase, metric].values[0] 
                        if phase in metrics_evolution['phase'].values else None 
                        for phase in phases_attendues]
            
            ax.plot(noms_phases, values, 'o-', linewidth=2, markersize=8)
            ax.set_title(f'Évolution du {title}')
            ax.set_xticks(range(len(noms_phases)))
            ax.set_xticklabels(noms_phases, rotation=45)
            ax.grid(True)
            
            if ymin is not None and ymax is not None:
                ax.set_ylim(ymin, ymax)
            
            # Ajout des valeurs sur les points
            for i, value in enumerate(values):
                if value is not None:
                    ax.annotate(f'{value:.3f}', 
                                (i, value), 
                                textcoords="offset points", 
                                xytext=(0,10), 
                                ha='center')

    plt.tight_layout()
    plt.show()

    # === 8. Analyse des améliorations ===
    display(pd.DataFrame({'': ['=== 8. ANALYSE DES AMÉLIORATIONS ===']}).style.hide(axis='index'))

    def test_amelioration(metrique, phase1, phase2):
        """Test si l'amélioration entre deux phases est significative"""
        val1 = metrics_evolution.loc[metrics_evolution['phase'] == phase1, metrique].values[0]
        val2 = metrics_evolution.loc[metrics_evolution['phase'] == phase2, metrique].values[0]
        amelioration_pct = ((val2 - val1) / abs(val1)) * 100
        return amelioration_pct

    # Calcul des améliorations relatives
    ameliorations = []
    for metric in ['r2', 'rmse', 'mae']:
        # Amélioration entre initial et with_outliers
        if all(phase in metrics_evolution['phase'].values for phase in ['initial', 'with_outliers']):
            amelioration = test_amelioration(metric, 'initial', 'with_outliers')
            ameliorations.append({
                'Métrique': metric.upper(),
                'Phases': 'Initial → Avec outliers',
                'Amélioration (%)': f"{amelioration:+.1f}%"
            })
        
        # Amélioration entre with_outliers et without_outliers
        if all(phase in metrics_evolution['phase'].values for phase in ['with_outliers', 'without_outliers']):
            amelioration = test_amelioration(metric, 'with_outliers', 'without_outliers')
            ameliorations.append({
                'Métrique': metric.upper(),
                'Phases': 'Avec outliers → Sans outliers',
                'Amélioration (%)': f"{amelioration:+.1f}%"
            })
        
        # Amélioration entre without_outliers et final
        if all(phase in metrics_evolution['phase'].values for phase in ['without_outliers', 'final']):
            amelioration = test_amelioration(metric, 'without_outliers', 'final')
            ameliorations.append({
                'Métrique': metric.upper(),
                'Phases': 'Sans outliers → Final',
                'Amélioration (%)': f"{amelioration:+.1f}%"
            })

    # Affichage du tableau des améliorations
    ameliorations_df = pd.DataFrame(ameliorations)
    display(ameliorations_df.style.hide(axis='index'))


print("Erreur: Données insuffisantes pour effectuer les analyses")
=== 5 - ANALYSE POST-IMPUTATION ET ÉVALUATION FINALE ===
Colonnes disponibles dans le DataFrame:
['is_genuine', 'diagonal', 'height_left', 'height_right', 'margin_low', 'margin_up', 'length', 'margin_low_missing']
=== 1. ANALYSE DES DISTRIBUTIONS ===
Nombre de valeurs imputées: 37
Nombre de valeurs originales: 1463
No description has been provided for this image
Test Statistique P-valeur Interprétation
Kolmogorov-Smirnov 0.356 1.35e-04 Distributions différentes
=== 2. VÉRIFICATION DE LA LINÉARITÉ ===
Attention: Aucune colonne attendue n'a été trouvée dans le DataFrame
Erreur: Données insuffisantes pour effectuer les analyses
In [31]:
# 5 - Évaluation et imputation des valeurs manquantes
display(pd.DataFrame({'': ['=== ÉVALUATION ET IMPUTATION DES VALEURS MANQUANTES ===']}).style.hide(axis='index'))

try:
	# 1. État initial des données et validation croisée
	display(pd.DataFrame({'': ['=== VALIDATION CROISÉE ET ÉTAT INITIAL ===']}).style.hide(axis='index'))
	
	# Validation croisée avec les données sans outliers
	cv = KFold(n_splits=10, shuffle=True, random_state=42)
	
	# Calcul des différents scores sur les données nettoyées
	cv_scores = cross_val_score(best_model, X_train_no_outliers, y_train_no_outliers, cv=cv)
	cv_mse = cross_val_score(best_model, X_train_no_outliers, y_train_no_outliers, 
							cv=cv, scoring='neg_mean_squared_error')
	cv_mae = cross_val_score(best_model, X_train_no_outliers, y_train_no_outliers, 
							cv=cv, scoring='neg_mean_absolute_error')
	
	rmse_scores = np.sqrt(-cv_mse)
	mae_scores = -cv_mae
	
	display(pd.DataFrame({
		'Métrique': ['R² scores', 'R² moyen (±écart-type)',
					'RMSE scores', 'RMSE moyen (±écart-type)',
					'MAE scores', 'MAE moyen (±écart-type)'],
		'Valeur': [str(cv_scores), f"{cv_scores.mean():.4f} (±{cv_scores.std() * 2:.4f})",
				  str(rmse_scores), f"{rmse_scores.mean():.4f} (±{rmse_scores.std() * 2:.4f})",
				  str(mae_scores), f"{mae_scores.mean():.4f} (±{mae_scores.std() * 2:.4f})"]
	}).style.hide(axis='index'))

	# État initial des données
	initial_stats = pd.DataFrame({
		'Métrique': [
			'Nombre total d\'observations',
			'Nombre de valeurs manquantes',
			'Pourcentage de valeurs manquantes',
			'Distribution des valeurs existantes (moyenne)',
			'Distribution des valeurs existantes (écart-type)'
		],
		'Valeur': [
			len(df),
			df['margin_low'].isna().sum(),
			f"{(df['margin_low'].isna().sum() / len(df) * 100):.2f}%",
			f"{df['margin_low'].mean():.4f}",
			f"{df['margin_low'].std():.4f}"
		]
	})
	display(initial_stats.style.hide(axis='index'))

	# 2. Performance sur les données d'entraînement
	display(pd.DataFrame({'': ["=== PERFORMANCE SUR L'ENTRAÎNEMENT ==="]}).style.hide(axis='index'))
	
	y_train_pred = best_model.predict(X_train_no_outliers)
	r2_train = best_model.score(X_train_no_outliers, y_train_no_outliers)
	mse_train = mean_squared_error(y_train_no_outliers, y_train_pred)
	rmse_train = np.sqrt(mse_train)
	mae_train = mean_absolute_error(y_train_no_outliers, y_train_pred)
	
	display(pd.DataFrame({
		'Métrique': [
			"R² sur données d'entraînement",
			"RMSE sur données d'entraînement",
			"MAE sur données d'entraînement"
		],
		'Valeur': [
			f"{r2_train:.4f}",
			f"{rmse_train:.4f}",
			f"{mae_train:.4f}"
		]
	}).style.hide(axis='index'))

	# 3. Performance sur les données de test
	display(pd.DataFrame({'': ['=== PERFORMANCE SUR LE TEST ===']}).style.hide(axis='index'))
	
	# Nettoyage des données de test
	X_test_clean = X_test.copy()
	y_test_clean = y_test.copy()
	
	common_index_test = X_test_clean.index.intersection(y_test_clean.index)
	X_test_clean = X_test_clean.loc[common_index_test]
	y_test_clean = y_test_clean.loc[common_index_test]
	
	mask_test = ~(X_test_clean.isna().any(axis=1) | y_test_clean.isna())
	X_test_clean = X_test_clean[mask_test]
	y_test_clean = y_test_clean[mask_test]
	
	# Calcul des métriques sur le test
	y_test_pred = best_model.predict(X_test_clean)
	r2_test = best_model.score(X_test_clean, y_test_clean)
	mse_test = mean_squared_error(y_test_clean, y_test_pred)
	rmse_test = np.sqrt(mse_test)
	mae_test = mean_absolute_error(y_test_clean, y_test_pred)
	
	display(pd.DataFrame({
		'Métrique': [
			"R² sur données de test",
			"RMSE sur données de test",
			"MAE sur données de test",
			"Nombre d'observations test"
		],
		'Valeur': [
			f"{r2_test:.4f}",
			f"{rmse_test:.4f}",
			f"{mae_test:.4f}",
			str(len(X_test_clean))
		]
	}).style.hide(axis='index'))

	# 4. Imputation des valeurs manquantes
	display(pd.DataFrame({'': ['=== IMPUTATION DES VALEURS MANQUANTES ===']}).style.hide(axis='index'))
	
	# Création d'une copie pour l'imputation
	df_impute = df.copy()
	missing_mask = df_impute['margin_low'].isna()
	X_missing = df_impute[missing_mask].drop(['margin_low', 'margin_low_missing'], axis=1)
	
	# Prédiction des valeurs manquantes avec le meilleur modèle
	imputed_values = best_model.predict(X_missing)
	df_impute.loc[missing_mask, 'margin_low'] = imputed_values
	
	# Statistiques sur les valeurs imputées
	imputation_stats = pd.DataFrame({
		'Statistique': [
			'Nombre de valeurs imputées',
			'Moyenne des valeurs imputées',
			'Écart-type des valeurs imputées',
			'Minimum imputé',
			'Maximum imputé',
			'Pourcentage du dataset imputé'
		],
		'Valeur': [
			len(imputed_values),
			f"{np.mean(imputed_values):.4f}",
			f"{np.std(imputed_values):.4f}",
			f"{np.min(imputed_values):.4f}",
			f"{np.max(imputed_values):.4f}",
			f"{(len(imputed_values)/len(df)*100):.2f}%"
		]
	})
	display(imputation_stats.style.hide(axis='index'))

	# 5. Analyse de la qualité de l'imputation
	display(pd.DataFrame({'': ['=== ANALYSE DE LA QUALITÉ DE L\'IMPUTATION ===']}).style.hide(axis='index'))
	
	# Comparaison des distributions
	plt.figure(figsize=(10, 6))
	sns.kdeplot(data=df_impute[~missing_mask]['margin_low'], label='Valeurs existantes', alpha=0.5)
	sns.kdeplot(data=pd.Series(imputed_values), label='Valeurs imputées', alpha=0.5)
	plt.title('Comparaison des distributions')
	plt.xlabel('margin_low')
	plt.ylabel('Densité')
	plt.legend()
	plt.show()
	
	# Test de Kolmogorov-Smirnov
	ks_stat, ks_pvalue = stats.ks_2samp(df_impute[~missing_mask]['margin_low'], imputed_values)
	
	display(pd.DataFrame({
		'Test': ['Kolmogorov-Smirnov'],
		'Statistique': [f"{ks_stat:.4f}"],
		'P-value': [f"{ks_pvalue:.4e}"],
		'Interprétation': ['Distributions similaires' if ks_pvalue > 0.05 else 'Distributions différentes']
	}).style.hide(axis='index'))

	# 6. Résumé final
	display(pd.DataFrame({'': ['=== RÉSUMÉ FINAL ===']}).style.hide(axis='index'))
	
	# Coefficients finaux
	coef_df = pd.DataFrame({
		'Variable': features,
		'Coefficient': best_model.named_steps['model'].coef_
	}).sort_values(by='Coefficient', ascending=False)
	display(coef_df.style.hide(axis='index'))
	
	summary = pd.DataFrame({
		'Aspect': [
			'Performance globale',
			'Validation croisée',
			'Performance test',
			'Imputation',
			'Qualité de l\'imputation',
			'Impact sur les données'
		],
		'Résultat': [
			f"R² train = {r2_train:.4f}, R² test = {r2_test:.4f}",
			f"R² CV = {cv_scores.mean():.4f} (±{cv_scores.std()*2:.4f})",
			f"RMSE = {rmse_test:.4f}, MAE = {mae_test:.4f}",
			f"{len(imputed_values)} valeurs imputées ({(len(imputed_values)/len(df)*100):.2f}%)",
			'Distributions similaires' if ks_pvalue > 0.05 else 'Distributions significativement différentes',
			f"Variable la plus influente: {coef_df.iloc[0]['Variable']} (coef = {coef_df.iloc[0]['Coefficient']:.4f})"
		]
	})
	display(summary.style.hide(axis='index'))

except Exception as e:
	display(pd.DataFrame({'Erreur': [str(e)]}).style.hide(axis='index'))
	raise
=== ÉVALUATION ET IMPUTATION DES VALEURS MANQUANTES ===
=== VALIDATION CROISÉE ET ÉTAT INITIAL ===
Métrique Valeur
R² scores [0.65814074 0.62725891 0.65584834 0.59822991 0.69574516 0.63621035 0.62494566 0.45114387 0.66022956 0.55007798]
R² moyen (±écart-type) 0.6158 (±0.1329)
RMSE scores [0.38773132 0.40941933 0.40474967 0.4110845 0.3757642 0.41876644 0.37593432 0.3634334 0.40913942 0.39278663]
RMSE moyen (±écart-type) 0.3949 (±0.0353)
MAE scores [0.30290574 0.32927007 0.3174117 0.3165843 0.2993498 0.32999946 0.27348121 0.29037249 0.33164429 0.30507165]
MAE moyen (±écart-type) 0.3096 (±0.0360)
Métrique Valeur
Nombre total d'observations 1500
Nombre de valeurs manquantes 0
Pourcentage de valeurs manquantes 0.00%
Distribution des valeurs existantes (moyenne) 4.4829
Distribution des valeurs existantes (écart-type) 0.6598
=== PERFORMANCE SUR L'ENTRAÎNEMENT ===
Métrique Valeur
R² sur données d'entraînement 0.6335
RMSE sur données d'entraînement 0.3927
MAE sur données d'entraînement 0.3074
=== PERFORMANCE SUR LE TEST ===
Métrique Valeur
R² sur données de test 0.5974
RMSE sur données de test 0.3919
MAE sur données de test 0.3056
Nombre d'observations test 146
=== IMPUTATION DES VALEURS MANQUANTES ===
Erreur
Found array with 0 sample(s) (shape=(0, 6)) while a minimum of 1 is required by RobustScaler.
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[31], line 117
    114 X_missing = df_impute[missing_mask].drop(['margin_low', 'margin_low_missing'], axis=1)
    116 # Prédiction des valeurs manquantes avec le meilleur modèle
--> 117 imputed_values = best_model.predict(X_missing)
    118 df_impute.loc[missing_mask, 'margin_low'] = imputed_values
    120 # Statistiques sur les valeurs imputées

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\pipeline.py:600, in Pipeline.predict(self, X, **params)
    598 if not _routing_enabled():
    599     for _, name, transform in self._iter(with_final=False):
--> 600         Xt = transform.transform(Xt)
    601     return self.steps[-1][1].predict(Xt, **params)
    603 # metadata routing enabled

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\utils\_set_output.py:316, in _wrap_method_output.<locals>.wrapped(self, X, *args, **kwargs)
    314 @wraps(f)
    315 def wrapped(self, X, *args, **kwargs):
--> 316     data_to_wrap = f(self, X, *args, **kwargs)
    317     if isinstance(data_to_wrap, tuple):
    318         # only wrap the first output for cross decomposition
    319         return_tuple = (
    320             _wrap_data_with_container(method, data_to_wrap[0], X, self),
    321             *data_to_wrap[1:],
    322         )

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\preprocessing\_data.py:1658, in RobustScaler.transform(self, X)
   1645 """Center and scale the data.
   1646 
   1647 Parameters
   (...)
   1655     Transformed array.
   1656 """
   1657 check_is_fitted(self)
-> 1658 X = self._validate_data(
   1659     X,
   1660     accept_sparse=("csr", "csc"),
   1661     copy=self.copy,
   1662     dtype=FLOAT_DTYPES,
   1663     force_writeable=True,
   1664     reset=False,
   1665     force_all_finite="allow-nan",
   1666 )
   1668 if sparse.issparse(X):
   1669     if self.with_scaling:

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\base.py:633, in BaseEstimator._validate_data(self, X, y, reset, validate_separately, cast_to_ndarray, **check_params)
    631         out = X, y
    632 elif not no_val_X and no_val_y:
--> 633     out = check_array(X, input_name="X", **check_params)
    634 elif no_val_X and not no_val_y:
    635     out = _check_y(y, **check_params)

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\utils\validation.py:1087, in check_array(array, accept_sparse, accept_large_sparse, dtype, order, copy, force_writeable, force_all_finite, ensure_2d, allow_nd, ensure_min_samples, ensure_min_features, estimator, input_name)
   1085     n_samples = _num_samples(array)
   1086     if n_samples < ensure_min_samples:
-> 1087         raise ValueError(
   1088             "Found array with %d sample(s) (shape=%s) while a"
   1089             " minimum of %d is required%s."
   1090             % (n_samples, array.shape, ensure_min_samples, context)
   1091         )
   1093 if ensure_min_features > 0 and array.ndim == 2:
   1094     n_features = array.shape[1]

ValueError: Found array with 0 sample(s) (shape=(0, 6)) while a minimum of 1 is required by RobustScaler.
📊 Résultats après imputation des données
Métriques de performance :
- \(R^2\) sur données d'entraînement : 0.613.
- \(R^2\) sur données de test : 0.629.

Analyse des hypothèses :
1. Linéarité : Respectée.
2. Normalité des résidus : Non respectée (Test Shapiro-Wilk, p-value = 1.509e-04).
3. Homoscédasticité : Non respectée (Test Breusch-Pagan, p-value ≈ 9.02e-27).
4. Indépendance des erreurs : Respectée (Durbin-Watson = 1.957).
5. Absence de multicolinéarité : Non respectée (VIF max = 170738.73).

Conclusion :
Bien que les \(R^2\) obtenus semblent modérés (~60 %), les violations des hypothèses fondamentales de la régression linéaire (normalité, homoscédasticité, multicolinéarité) ces résultats rendent l'imputation des valeurs manquantes peu fiables, bien qu'effectuée correctement l'imputation risque d'introduir des biais significatifs qui affectent la stabilité des coefficients et l'interprétation globale du modèle.

¶

3.2 - Imputation avec selection des variables explicatives
In [27]:
# 5 - Évaluation et imputation des valeurs manquantes
display(pd.DataFrame({'': ['=== ÉVALUATION ET IMPUTATION DES VALEURS MANQUANTES ===']}).style.hide(axis='index'))

try:
	# 1. État initial des données et validation croisée
	display(pd.DataFrame({'': ['=== VALIDATION CROISÉE ET ÉTAT INITIAL ===']}).style.hide(axis='index'))
	
	# Validation croisée avec les données transformées
	cv = KFold(n_splits=10, shuffle=True, random_state=42)
	
	# Calcul des différents scores sur les données nettoyées
	cv_scores = cross_val_score(best_model, X_train_no_outliers, y_train_no_outliers, cv=cv)
	cv_mse = cross_val_score(best_model, X_train_no_outliers, y_train_no_outliers, 
							cv=cv, scoring='neg_mean_squared_error')
	cv_mae = cross_val_score(best_model, X_train_no_outliers, y_train_no_outliers, 
							cv=cv, scoring='neg_mean_absolute_error')
	
	rmse_scores = np.sqrt(-cv_mse)
	mae_scores = -cv_mae
	
	display(pd.DataFrame({
		'Métrique': ['R² scores', 'R² moyen (±écart-type)',
					'RMSE scores', 'RMSE moyen (±écart-type)',
					'MAE scores', 'MAE moyen (±écart-type)'],
		'Valeur': [str(cv_scores), f"{cv_scores.mean():.4f} (±{cv_scores.std() * 2:.4f})",
				  str(rmse_scores), f"{rmse_scores.mean():.4f} (±{rmse_scores.std() * 2:.4f})",
				  str(mae_scores), f"{mae_scores.mean():.4f} (±{mae_scores.std() * 2:.4f})"]
	}).style.hide(axis='index'))

	# État initial des données
	initial_stats = pd.DataFrame({
		'Métrique': [
			'Nombre total d\'observations',
			'Nombre de valeurs manquantes',
			'Pourcentage de valeurs manquantes',
			'Distribution des valeurs existantes (moyenne)',
			'Distribution des valeurs existantes (écart-type)'
		],
		'Valeur': [
			len(df),
			df['margin_low'].isna().sum(),
			f"{(df['margin_low'].isna().sum() / len(df) * 100):.2f}%",
			f"{df['margin_low'].mean():.4f}",
			f"{df['margin_low'].std():.4f}"
		]
	})
	display(initial_stats.style.hide(axis='index'))

	# 2. Performance sur les données d'entraînement
	display(pd.DataFrame({'': ["=== PERFORMANCE SUR L'ENTRAÎNEMENT ==="]}).style.hide(axis='index'))
	
	# Utilisation des données transformées pour la prédiction
	X_train_transformed = best_model.named_steps['scaler'].transform(X_train_no_outliers)
	y_train_pred = best_model.named_steps['model'].predict(X_train_transformed)
	r2_train = r2_score(y_train_no_outliers, y_train_pred)
	mse_train = mean_squared_error(y_train_no_outliers, y_train_pred)
	rmse_train = np.sqrt(mse_train)
	mae_train = mean_absolute_error(y_train_no_outliers, y_train_pred)
	
	display(pd.DataFrame({
		'Métrique': [
			"R² sur données d'entraînement",
			"RMSE sur données d'entraînement",
			"MAE sur données d'entraînement"
		],
		'Valeur': [
			f"{r2_train:.4f}",
			f"{rmse_train:.4f}",
			f"{mae_train:.4f}"
		]
	}).style.hide(axis='index'))

	# 3. Performance sur les données de test
	display(pd.DataFrame({'': ['=== PERFORMANCE SUR LE TEST ===']}).style.hide(axis='index'))
	
	# Nettoyage des données de test
	X_test_clean = X_test.copy()
	y_test_clean = y_test.copy()
	
	common_index_test = X_test_clean.index.intersection(y_test_clean.index)
	X_test_clean = X_test_clean.loc[common_index_test]
	y_test_clean = y_test_clean.loc[common_index_test]
	
	mask_test = ~(X_test_clean.isna().any(axis=1) | y_test_clean.isna())
	X_test_clean = X_test_clean[mask_test]
	y_test_clean = y_test_clean[mask_test]
	
	# Calcul des métriques sur le test avec données transformées
	X_test_transformed = best_model.named_steps['scaler'].transform(X_test_clean)
	y_test_pred = best_model.named_steps['model'].predict(X_test_transformed)
	r2_test = r2_score(y_test_clean, y_test_pred)
	mse_test = mean_squared_error(y_test_clean, y_test_pred)
	rmse_test = np.sqrt(mse_test)
	mae_test = mean_absolute_error(y_test_clean, y_test_pred)
	
	display(pd.DataFrame({
		'Métrique': [
			"R² sur données de test",
			"RMSE sur données de test",
			"MAE sur données de test",
			"Nombre d'observations test"
		],
		'Valeur': [
			f"{r2_test:.4f}",
			f"{rmse_test:.4f}",
			f"{mae_test:.4f}",
			str(len(X_test_clean))
		]
	}).style.hide(axis='index'))

	# 4. Imputation des valeurs manquantes
	display(pd.DataFrame({'': ['=== IMPUTATION DES VALEURS MANQUANTES ===']}).style.hide(axis='index'))
	
	# Préparation des données pour l'imputation
	df_impute = df.copy()
	missing_mask = df_impute['margin_low'].isna()
	X_missing = df_impute[missing_mask].drop(['margin_low', 'margin_low_missing'], axis=1)
	
	# Prédiction des valeurs manquantes avec le meilleur modèle
	X_missing_transformed = best_model.named_steps['scaler'].transform(X_missing)
	imputed_values = best_model.named_steps['model'].predict(X_missing_transformed)
	df_impute.loc[missing_mask, 'margin_low'] = imputed_values

	# Calcul des intervalles de confiance pour les imputations
	def calculate_prediction_intervals(model, X, y_pred, X_train, y_train, alpha=0.05):
		# Ajout d'une constante pour l'intercept
		X_const = sm.add_constant(X)
		X_train_const = sm.add_constant(X_train)

		# Calcul de la variance des résidus
		y_train_pred = model.predict(X_train)
		mse = mean_squared_error(y_train, y_train_pred)

		# Calcul de la matrice de variance-covariance
		var_covar_matrix = mse * np.linalg.inv(X_train_const.T.dot(X_train_const))

		# Calcul des erreurs standards pour chaque prédiction
		std_errors = np.sqrt(np.diagonal(X_const.dot(var_covar_matrix).dot(X_const.T)))

		# Calcul des intervalles
		t_value = stats.t.ppf(1 - alpha/2, len(y_train) - X_train.shape[1])

		return pd.DataFrame({
			'prediction': y_pred,
			'lower_bound': y_pred - t_value * std_errors,
			'upper_bound': y_pred + t_value * std_errors,
			'std_error': std_errors
		})

	imputation_intervals = calculate_prediction_intervals(
		best_model.named_steps['model'],
		X_missing_transformed,
		imputed_values,
		best_model.named_steps['scaler'].transform(X_train_no_outliers),
		y_train_no_outliers
	)
	
	# Statistiques sur les valeurs imputées
	imputation_stats = pd.DataFrame({
		'Statistique': [
			'Nombre de valeurs imputées',
			'Moyenne des valeurs imputées',
			'Écart-type des valeurs imputées',
			'Minimum imputé',
			'Maximum imputé',
			'Pourcentage du dataset imputé',
			'Largeur moyenne des intervalles de confiance'
		],
		'Valeur': [
			len(imputed_values),
			f"{np.mean(imputed_values):.4f}",
			f"{np.std(imputed_values):.4f}",
			f"{np.min(imputed_values):.4f}",
			f"{np.max(imputed_values):.4f}",
			f"{(len(imputed_values)/len(df)*100):.2f}%",
			f"{np.mean(imputation_intervals['upper_bound'] - imputation_intervals['lower_bound']):.4f}"
		]
	})
	display(imputation_stats.style.hide(axis='index'))

	# 5. Analyse de la qualité de l'imputation
	display(pd.DataFrame({'': ['=== ANALYSE DE LA QUALITÉ DE L\'IMPUTATION ===']}).style.hide(axis='index'))
	
	# Comparaison des distributions
	plt.figure(figsize=(10, 6))
	sns.kdeplot(data=df_impute[~missing_mask]['margin_low'], label='Valeurs existantes', alpha=0.5)
	sns.kdeplot(data=pd.Series(imputed_values), label='Valeurs imputées', alpha=0.5)
	plt.title('Comparaison des distributions')
	plt.xlabel('margin_low')
	plt.ylabel('Densité')
	plt.legend()
	plt.show()
	
	# Test de Kolmogorov-Smirnov
	ks_stat, ks_pvalue = stats.ks_2samp(df_impute[~missing_mask]['margin_low'], imputed_values)
	
	display(pd.DataFrame({
		'Test': ['Kolmogorov-Smirnov'],
		'Statistique': [f"{ks_stat:.4f}"],
		'P-value': [f"{ks_pvalue:.4e}"],
		'Interprétation': ['Distributions similaires' if ks_pvalue > 0.05 else 'Distributions différentes']
	}).style.hide(axis='index'))

	# 6. Résumé final avec métriques de tracking
	display(pd.DataFrame({'': ['=== RÉSUMÉ FINAL ===']}).style.hide(axis='index'))
	
	# Coefficients finaux
	coef_df = pd.DataFrame({
		'Variable': features,
		'Coefficient': best_model.named_steps['model'].coef_
	}).sort_values(by='Coefficient', ascending=False)
	display(coef_df.style.hide(axis='index'))
	
	# Ajout des métriques finales au tracker
	final_metrics = {
	'phase': 'imputation',
	'r2': r2_test,
	'rmse': rmse_test,
	'mae': mae_test,
	'het_pvalue': het_p_value,  # Ajouter après recalcul du test Breusch-Pagan
	'shapiro_pvalue': shapiro_p,  # Ajouter après test de normalité
	'interval_coverage': np.mean(
		(imputed_values >= imputation_intervals['lower_bound']) & 
		(imputed_values <= imputation_intervals['upper_bound'])
	),
	'interval_width': np.mean(
		imputation_intervals['upper_bound'] - imputation_intervals['lower_bound']
	)
}

	# Ajouter le calcul de l'hétéroscédasticité avant :
	_, het_p_value, _, _ = het_breuschpagan(
		y_test_clean - y_test_pred,
		sm.add_constant(X_test_transformed)
)
	
	# Affichage du résumé final
	summary = pd.DataFrame({
		'Aspect': [
			'Performance globale',
			'Validation croisée',
			'Performance test',
			'Imputation',
			'Qualité de l\'imputation',
			'Impact sur les données'
		],
		'Résultat': [
			f"R² train = {r2_train:.4f}, R² test = {r2_test:.4f}",
			f"R² CV = {cv_scores.mean():.4f} (±{cv_scores.std()*2:.4f})",
			f"RMSE = {rmse_test:.4f}, MAE = {mae_test:.4f}",
			f"{len(imputed_values)} valeurs imputées ({(len(imputed_values)/len(df)*100):.2f}%)",
			'Distributions similaires' if ks_pvalue > 0.05 else 'Distributions significativement différentes',
			f"Variable la plus influente: {coef_df.iloc[0]['Variable']} (coef = {coef_df.iloc[0]['Coefficient']:.4f})"
		]
	})
	display(summary.style.hide(axis='index'))

	# Visualisation finale de l'évolution des métriques
	plot_metrics_evolution(tracker)  # Réintégration de la visualisation
	display(pd.DataFrame({'': ['=== ÉVOLUTION DES MÉTRIQUES ===']}).style.hide(axis='index'))
	display(tracker.get_summary().style.hide(axis='index'))

except Exception as e:
	display(pd.DataFrame({'Erreur': [str(e)]}).style.hide(axis='index'))
	print(f"Erreur détaillée : {e}")  # Pour le debugging
	raise
=== ÉVALUATION ET IMPUTATION DES VALEURS MANQUANTES ===
=== VALIDATION CROISÉE ET ÉTAT INITIAL ===
Métrique Valeur
R² scores [0.6518257 0.73338966 0.65347531 0.6219288 0.63484962 0.65744788 0.54642133 0.57333442 0.44625612 0.62138849]
R² moyen (±écart-type) 0.6140 (±0.1470)
RMSE scores [0.40058199 0.32658501 0.37109361 0.38482367 0.40566226 0.39967054 0.4201234 0.42043812 0.41541963 0.38441502]
RMSE moyen (±écart-type) 0.3929 (±0.0540)
MAE scores [0.30699117 0.25736309 0.29754534 0.29362255 0.30207377 0.30865241 0.33378001 0.34030226 0.34831388 0.31100653]
MAE moyen (±écart-type) 0.3100 (±0.0499)
Métrique Valeur
Nombre total d'observations 1500
Nombre de valeurs manquantes 37
Pourcentage de valeurs manquantes 2.47%
Distribution des valeurs existantes (moyenne) 4.4860
Distribution des valeurs existantes (écart-type) 0.6638
=== PERFORMANCE SUR L'ENTRAÎNEMENT ===
Métrique Valeur
R² sur données d'entraînement 0.6276
RMSE sur données d'entraînement 0.3908
MAE sur données d'entraînement 0.3076
=== PERFORMANCE SUR LE TEST ===
Métrique Valeur
R² sur données de test 0.6284
RMSE sur données de test 0.4129
MAE sur données de test 0.3118
Nombre d'observations test 292
=== IMPUTATION DES VALEURS MANQUANTES ===
Statistique Valeur
Nombre de valeurs imputées 37
Moyenne des valeurs imputées 4.3604
Écart-type des valeurs imputées 0.4503
Minimum imputé 4.0277
Maximum imputé 5.2776
Pourcentage du dataset imputé 2.47%
Largeur moyenne des intervalles de confiance 0.1196
=== ANALYSE DE LA QUALITÉ DE L'IMPUTATION ===
No description has been provided for this image
Test Statistique P-value Interprétation
Kolmogorov-Smirnov 0.3634 8.9161e-05 Distributions différentes
=== RÉSUMÉ FINAL ===
Variable Coefficient
length 0.007021
height_left 0.005155
diagonal -0.001896
height_right -0.007796
margin_up -0.055088
is_genuine -1.137840
Aspect Résultat
Performance globale R² train = 0.6276, R² test = 0.6284
Validation croisée R² CV = 0.6140 (±0.1470)
Performance test RMSE = 0.4129, MAE = 0.3118
Imputation 37 valeurs imputées (2.47%)
Qualité de l'imputation Distributions significativement différentes
Impact sur les données Variable la plus influente: length (coef = 0.0070)
=== ÉVOLUTION DES MÉTRIQUES ===
phase r2 rmse mae dw_test het_pvalue shapiro_pvalue interval_coverage interval_width
initial nan nan nan 1.956608 0.000000 0.000151 nan nan
with_outliers 0.613034 0.410596 0.317217 nan nan nan 1.000000 1.248208
without_outliers 0.627601 0.390761 0.307563 nan nan nan 1.000000 1.210255
validation 0.627601 nan nan 1.900947 0.000000 0.196417 1.000000 1.210255
No description has been provided for this image
📉 Réduction de dimensionnalité par ACP
Objectif :
- Éliminer la multicolinéarité (VIF élevé) pour permettre l’imputation des valeurs manquantes via régression linéaire.

Pourquoi utiliser l'ACP dans ce contexte ?
1. Réduction de la multicolinéarité : Les composantes principales sont indépendantes (orthogonales), ce qui garantit un VIF = 1 pour chaque composante.
2. Amélioration des prédictions : Stabilise les coefficients estimés lors de la régression linéaire.

Étapes réalisées :
1. Centrage et réduction des données pour éliminer les biais dus aux échelles différentes.
2. Application de l’ACP pour produire des variables indépendantes (composantes principales).
3. Utilisation des composantes principales comme prédicteurs dans la régression pour l’imputation des valeurs manquantes.

Avantages :
- Suppression de la multicolinéarité, garantissant la validité des coefficients.
- Préparation robuste des données pour l’imputation.

Conclusion :
Dans ce cadre, l’ACP est utilisée comme un outil technique de réduction de la multicolinéarité, l’objectif est d’améliorer la stabilité et la fiabilité des imputations.
In [24]:
# === PRÉPARATION DES DONNÉES ===
display(pd.DataFrame({'': ['=== PRÉPARATION DES DONNÉES ===']}).style.hide(axis='index'))

# Calcul du DataFrame Dask si nécessaire
if hasattr(df, 'compute'):
	df = df.compute()

# Transformation de la variable booléenne en entier
df['is_genuine'] = df['is_genuine'].astype(int)

# Stockage des valeurs manquantes pour margin_low
df['margin_low_missing'] = df['margin_low'].isna().astype(int)

# === ACP : Transformation des variables explicatives ===
# Sélection des colonnes numériques à inclure dans l'ACP
variables_pour_acp = df.select_dtypes(include=[np.number]).drop(
	columns=['margin_low', 'margin_low_missing', 'is_genuine'], errors='ignore'
)

# Vérification qu'il n'y a pas de valeurs manquantes dans les variables sélectionnées
assert variables_pour_acp.notna().all().all(), "Des valeurs manquantes existent dans les colonnes pour l'ACP."

# Normalisation des données
scaler = RobustScaler()
variables_normalisées = scaler.fit_transform(variables_pour_acp)

# Application de l'ACP
pca = PCA()
composantes_principales = pca.fit_transform(variables_normalisées)

# Ajout des composantes principales au DataFrame
pca_columns = [f'PC{i+1}' for i in range(composantes_principales.shape[1])]
df_pca = pd.DataFrame(composantes_principales, columns=pca_columns, index=df.index)
df = pd.concat([df, df_pca], axis=1)

# === DIVISION DES DONNÉES EN TRAIN/TEST ===
# Sélection des colonnes explicatives finales : composantes principales + is_genuine
X = df[pca_columns + ['is_genuine']]
y = df['margin_low']

# Division en datasets avec stratification
X_train, X_test, y_train, y_test = train_test_split(
	X, y,
	test_size=0.2,
	random_state=42,
	stratify=df[['is_genuine', 'margin_low_missing']]
)

# Suppression des colonnes auxiliaires inutiles après stratification
X_train = X_train.drop(columns=['is_genuine'])
X_test = X_test.drop(columns=['is_genuine'])

# === VÉRIFICATIONS DES DONNÉES PRÉPARÉES ===
results = []

# Section : Dimensions des datasets
results.append({'Section': 'Dimensions des données',
				'Description': 'X_train',
				'Valeur': f"{X_train.shape[0]} observations, {X_train.shape[1]} variables"})
results.append({'Section': 'Dimensions des données',
				'Description': 'X_test',
				'Valeur': f"{X_test.shape[0]} observations, {X_test.shape[1]} variables"})

# Section : Valeurs manquantes dans X_train et X_test
results.append({'Section': '=== VALEURS MANQUANTES DANS LES VARIABLES EXPLICATIVES ===',
				'Description': '', 'Valeur': ''})

for col in X_train.columns:
	results.append({
		'Section': 'X_train - Valeurs manquantes',
		'Description': col,
		'Valeur': f"{X_train[col].isna().sum()} ({(X_train[col].isna().sum() / len(X_train) * 100):.1f}%)"
	})

for col in X_test.columns:
	results.append({
		'Section': 'X_test - Valeurs manquantes',
		'Description': col,
		'Valeur': f"{X_test[col].isna().sum()} ({(X_test[col].isna().sum() / len(X_test) * 100):.1f}%)"
	})

# Section : Valeurs manquantes dans la variable cible
results.append({'Section': '=== VALEURS MANQUANTES DANS LA VARIABLE CIBLE (margin_low) ===',
				'Description': '', 'Valeur': ''})
results.append({'Section': 'Variable cible - Valeurs manquantes',
				'Description': 'y_train',
				'Valeur': f"{y_train.isna().sum()} NaN ({(y_train.isna().sum() / len(y_train) * 100):.1f}%)"})
results.append({'Section': 'Variable cible - Valeurs manquantes',
				'Description': 'y_test',
				'Valeur': f"{y_test.isna().sum()} NaN ({(y_test.isna().sum() / len(y_test) * 100):.1f}%)"})

# Section : Distribution des classes (is_genuine)
results.append({'Section': '=== DISTRIBUTION DES CLASSES (is_genuine) ===',
				'Description': '', 'Valeur': ''})

train_genuine = df.loc[X_train.index, 'is_genuine'].value_counts(normalize=True)[1]
train_fake = df.loc[X_train.index, 'is_genuine'].value_counts(normalize=True)[0]
results.append({'Section': 'Distribution train',
				'Description': 'Billet de banque authentique (1)',
				'Valeur': f"{train_genuine:.1%} observations"})
results.append({'Section': 'Distribution train',
				'Description': 'Billets de banque contrefait (0)',
				'Valeur': f"{train_fake:.1%} observations"})

test_genuine = df.loc[X_test.index, 'is_genuine'].value_counts(normalize=True)[1]
test_fake = df.loc[X_test.index, 'is_genuine'].value_counts(normalize=True)[0]
results.append({'Section': 'Distribution test',
				'Description': 'Billet de banque authentique (1)',
				'Valeur': f"{test_genuine:.1%} observations"})
results.append({'Section': 'Distribution test',
				'Description': 'Billets de banque contrefait (0)',
				'Valeur': f"{test_fake:.1%} observations"})

# Création et affichage du DataFrame final
results_df = pd.DataFrame(results)
display(results_df.style.hide(axis='index'))
=== PRÉPARATION DES DONNÉES ===
Section Description Valeur
Dimensions des données X_train 1200 observations, 5 variables
Dimensions des données X_test 300 observations, 5 variables
=== VALEURS MANQUANTES DANS LES VARIABLES EXPLICATIVES ===
X_train - Valeurs manquantes PC1 0 (0.0%)
X_train - Valeurs manquantes PC2 0 (0.0%)
X_train - Valeurs manquantes PC3 0 (0.0%)
X_train - Valeurs manquantes PC4 0 (0.0%)
X_train - Valeurs manquantes PC5 0 (0.0%)
X_test - Valeurs manquantes PC1 0 (0.0%)
X_test - Valeurs manquantes PC2 0 (0.0%)
X_test - Valeurs manquantes PC3 0 (0.0%)
X_test - Valeurs manquantes PC4 0 (0.0%)
X_test - Valeurs manquantes PC5 0 (0.0%)
=== VALEURS MANQUANTES DANS LA VARIABLE CIBLE (margin_low) ===
Variable cible - Valeurs manquantes y_train 29 NaN (2.4%)
Variable cible - Valeurs manquantes y_test 8 NaN (2.7%)
=== DISTRIBUTION DES CLASSES (is_genuine) ===
Distribution train Billet de banque authentique (1) 66.7% observations
Distribution train Billets de banque contrefait (0) 33.3% observations
Distribution test Billet de banque authentique (1) 66.7% observations
Distribution test Billets de banque contrefait (0) 33.3% observations
In [25]:
# Filtrer les colonnes numériques
df_numeric = df.select_dtypes(include=[np.number])

# Normaliser les données avec RobustScaler
scaler = RobustScaler()
df_normalized = scaler.fit_transform(df_numeric)

# Appliquer l'ACP
pca = PCA()
df_pca = pca.fit_transform(df_normalized)

# Stocker les composantes principales et les vecteurs propres
components = pca.components_
explained_variance = pca.explained_variance_ratio_
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[25], line 10
      8 # Appliquer l'ACP
      9 pca = PCA()
---> 10 df_pca = pca.fit_transform(df_normalized)
     12 # Stocker les composantes principales et les vecteurs propres
     13 components = pca.components_

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\daal4py\sklearn\_n_jobs_support.py:128, in _run_with_n_jobs.<locals>.method_wrapper(self, *args, **kwargs)
    123     logger.debug(
    124         f"{cl.__module__}.{cl.__name__}.{method.__name__}: "
    125         f"setting {n_jobs} threads (previous - {old_n_threads})"
    126     )
    127     set_n_threads(n_jobs)
--> 128 result = method(self, *args, **kwargs)
    129 if n_jobs != old_n_threads:
    130     set_n_threads(old_n_threads)

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\sklearn\utils\_set_output.py:316, in _wrap_method_output.<locals>.wrapped(self, X, *args, **kwargs)
    314 @wraps(f)
    315 def wrapped(self, X, *args, **kwargs):
--> 316     data_to_wrap = f(self, X, *args, **kwargs)
    317     if isinstance(data_to_wrap, tuple):
    318         # only wrap the first output for cross decomposition
    319         return_tuple = (
    320             _wrap_data_with_container(method, data_to_wrap[0], X, self),
    321             *data_to_wrap[1:],
    322         )

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\sklearnex\decomposition\pca.py:192, in PCA.fit_transform(self, X, y)
    190 def fit_transform(self, X, y=None):
    191     if sklearn_check_version("1.5"):
--> 192         U, S, Vt, X_fit, x_is_centered, xp = self._fit(X)
    193     else:
    194         U, S, Vt = self._fit(X)

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\sklearnex\_device_offload.py:102, in wrap_output_data.<locals>.wrapper(self, *args, **kwargs)
    100 @wraps(func)
    101 def wrapper(self, *args, **kwargs):
--> 102     result = func(self, *args, **kwargs)
    103     if not (len(args) == 0 and len(kwargs) == 0):
    104         data = (*args, *kwargs.values())

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\sklearnex\decomposition\pca.py:125, in PCA._fit(self, X)
    117 elif sklearn_check_version("1.1"):
    118     check_scalar(
    119         self.n_oversamples,
    120         "n_oversamples",
    121         min_val=1,
    122         target_type=numbers.Integral,
    123     )
--> 125 return dispatch(
    126     self,
    127     "fit",
    128     {
    129         "onedal": self.__class__._onedal_fit,
    130         "sklearn": sklearn_PCA._fit,
    131     },
    132     X,
    133 )

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\sklearnex\_device_offload.py:74, in dispatch(obj, method_name, branches, *args, **kwargs)
     72 if backend == "onedal":
     73     patching_status.write_log(queue=q)
---> 74     return branches[backend](obj, *hostargs, **hostkwargs, queue=q)
     75 if backend == "sklearn":
     76     if (
     77         "array_api_dispatch" in get_config()
     78         and get_config()["array_api_dispatch"]
   (...)
     82         # If `array_api_dispatch` enabled and array api is supported for the stock scikit-learn,
     83         # then raw inputs are used for the fallback.

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\sklearnex\decomposition\pca.py:136, in PCA._onedal_fit(self, X, queue)
    135 def _onedal_fit(self, X, queue=None):
--> 136     X = self._validate_data(
    137         X,
    138         dtype=[np.float64, np.float32],
    139         ensure_2d=True,
    140         copy=self.copy,
    141     )
    143     onedal_params = {
    144         "n_components": self.n_components,
    145         "is_deterministic": True,
    146         "method": "svd" if self._fit_svd_solver == "onedal_svd" else "cov",
    147         "whiten": self.whiten,
    148     }
    149     self._onedal_estimator = onedal_PCA(**onedal_params)

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\sklearn\base.py:633, in BaseEstimator._validate_data(self, X, y, reset, validate_separately, cast_to_ndarray, **check_params)
    631         out = X, y
    632 elif not no_val_X and no_val_y:
--> 633     out = check_array(X, input_name="X", **check_params)
    634 elif no_val_X and not no_val_y:
    635     out = _check_y(y, **check_params)

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\sklearn\utils\validation.py:1064, in check_array(array, accept_sparse, accept_large_sparse, dtype, order, copy, force_writeable, force_all_finite, ensure_2d, allow_nd, ensure_min_samples, ensure_min_features, estimator, input_name)
   1058     raise ValueError(
   1059         "Found array with dim %d. %s expected <= 2."
   1060         % (array.ndim, estimator_name)
   1061     )
   1063 if force_all_finite:
-> 1064     _assert_all_finite(
   1065         array,
   1066         input_name=input_name,
   1067         estimator_name=estimator_name,
   1068         allow_nan=force_all_finite == "allow-nan",
   1069     )
   1071 if copy:
   1072     if _is_numpy_namespace(xp):
   1073         # only make a copy if `array` and `array_orig` may share memory`

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\daal4py\sklearn\utils\validation.py:64, in _assert_all_finite(X, allow_nan, msg_dtype, estimator_name, input_name)
     62 if X.size < 32768:
     63     if sklearn_check_version("1.1"):
---> 64         _sklearn_assert_all_finite(
     65             X,
     66             allow_nan=allow_nan,
     67             msg_dtype=msg_dtype,
     68             estimator_name=estimator_name,
     69             input_name=input_name,
     70         )
     71     else:
     72         _sklearn_assert_all_finite(X, allow_nan=allow_nan, msg_dtype=msg_dtype)

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\sklearn\utils\validation.py:123, in _assert_all_finite(X, allow_nan, msg_dtype, estimator_name, input_name)
    120 if first_pass_isfinite:
    121     return
--> 123 _assert_all_finite_element_wise(
    124     X,
    125     xp=xp,
    126     allow_nan=allow_nan,
    127     msg_dtype=msg_dtype,
    128     estimator_name=estimator_name,
    129     input_name=input_name,
    130 )

File c:\mamba\envs\MambaRapids_3_9\lib\site-packages\sklearn\utils\validation.py:172, in _assert_all_finite_element_wise(X, xp, allow_nan, msg_dtype, estimator_name, input_name)
    155 if estimator_name and input_name == "X" and has_nan_error:
    156     # Improve the error message on how to handle missing values in
    157     # scikit-learn.
    158     msg_err += (
    159         f"\n{estimator_name} does not accept missing values"
    160         " encoded as NaN natively. For supervised learning, you might want"
   (...)
    170         "#estimators-that-handle-nan-values"
    171     )
--> 172 raise ValueError(msg_err)

ValueError: Input X contains NaN.
PCA does not accept missing values encoded as NaN natively. For supervised learning, you might want to consider sklearn.ensemble.HistGradientBoostingClassifier and Regressor which accept missing values encoded as NaNs natively. Alternatively, it is possible to preprocess the data, for instance by using an imputer transformer in a pipeline or drop samples with missing values. See https://scikit-learn.org/stable/modules/impute.html You can find a list of all estimators that handle NaN values at the following page: https://scikit-learn.org/stable/modules/impute.html#estimators-that-handle-nan-values
In [ ]:
# Identifier les colonnes avec des valeurs manquantes
missing_mask = df_numeric.isna()

# Créer un masque pour les valeurs non manquantes
not_missing_mask = ~missing_mask

# Diviser les données selon la disponibilité des valeurs manquantes
X_train_pca = df_pca[not_missing_mask.any(axis=1)]  # Lignes sans valeurs manquantes
y_train = df_numeric[not_missing_mask.any(axis=1)]  # Correspondance dans l'espace original

X_test_pca = df_pca[missing_mask.any(axis=1)]  # Lignes avec valeurs manquantes
In [ ]:
# Calculer la variance expliquée cumulée
explained_variance = pca.explained_variance_

cumulative_explained_variance = np.cumsum(explained_variance)

# Créer un scree plot avec la variance expliquée cumulée
plt.figure(figsize=(6, 4))
plt.plot(range(1, len(explained_variance) + 1), explained_variance, 'o-', label='Variance expliquée par chaque composante')
plt.plot(range(1, len(explained_variance) + 1), cumulative_explained_variance, 'o-', label='Variance expliquée cumulée')
plt.title('Scree Plot')
plt.xlabel('Nombre de composantes principales')
plt.ylabel('Proportion de la variance expliquée')
plt.legend()
plt.show()

¶

4 - MODÉLISATION

¶

4.1 - Classification avec Kmeans
In [10]:
# === DIVISION DES DONNÉES ET ACP ===

# Supprimer les lignes avec des valeurs manquantes
df.dropna(inplace=True)

# Convertir la colonne 'is_genuine' en type entier
df['is_genuine'] = df['is_genuine'].astype(int)

# Diviser les données en ensembles d'entraînement et de test avec stratification
X = df.drop(columns=['is_genuine'])
y = df['is_genuine']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)

# Sélectionner les colonnes numériques pour l'ACP
numerical_features_train = X_train.select_dtypes(include=[np.number])

# Normaliser les données d'entraînement avec RobustScaler
robust_scaler = RobustScaler()
robust_scaled_data_train = robust_scaler.fit_transform(numerical_features_train)

# Normaliser les données d'entraînement avec StandardScaler
standard_scaler = StandardScaler()
standard_scaled_data_train = standard_scaler.fit_transform(numerical_features_train)

# Appliquer l'ACP sur les données normalisées avec RobustScaler
pca_robust = PCA()
pca_result_robust = pca_robust.fit_transform(robust_scaled_data_train)

# Appliquer l'ACP sur les données normalisées avec StandardScaler
pca_standard = PCA()
pca_result_standard = pca_standard.fit_transform(standard_scaled_data_train)

# Afficher la variance expliquée par chaque composante principale (RobustScaler)
print("Variance expliquée par chaque composante principale (RobustScaler) :")
print(pca_robust.explained_variance_ratio_)

# === VISUALISATION DE LA VARIANCE EXPLIQUÉE ===

# Scree plot
plt.figure(figsize=(6, 4))
plt.plot(range(1, pca_robust.n_components_ + 1), pca_robust.explained_variance_ratio_, marker='o')
plt.title('Scree plot (RobustScaler)')
plt.xlabel('Composantes principales')
plt.ylabel('Variance expliquée')
plt.show()

# Variance expliquée cumulée
plt.figure(figsize=(6, 4))
plt.plot(range(1, pca_robust.n_components_ + 1), np.cumsum(pca_robust.explained_variance_ratio_), marker='o')
plt.title('Variance expliquée cumulée (RobustScaler)')
plt.xlabel('Composantes principales')
plt.ylabel('Variance expliquée cumulée')
plt.show()
Variance expliquée par chaque composante principale (RobustScaler) :
[0.426768   0.17348044 0.13580336 0.11572394 0.09822831 0.04999594]
No description has been provided for this image
No description has been provided for this image
In [11]:
# === CHOIX DU NOMBRE DE COMPOSANTES PRINCIPALES ===

n_components = 2 

n_clusters = 2
In [12]:
# === APPLICATION SUR L'ENSEMBLE D'ENTRAÎNEMENT ET VISUALISATION ===

def cluster_stats(X_data, y_data, cluster_column):
    """
    Calcule les statistiques descriptives pour chaque cluster.

    Args:
        X_data: Le DataFrame des données.
        y_data: Les labels des données.
        cluster_column: Le nom de la colonne contenant les clusters.
    """
    for cluster in X_data[cluster_column].unique():
        print(f"\nCluster {cluster}:")
        print(X_data[X_data[cluster_column] == cluster].describe())
        # Filtrer y_data en utilisant les mêmes indices que X_data
        cluster_indices = X_data[cluster_column] == cluster
        proportion = y_data[cluster_indices].mean()
        print(f"\nProportion de billets authentiques (is_genuine = 1) dans le cluster {cluster}: {proportion}")


def model_metrics(y_true, y_pred, scaler_name):
    """
    Calcule les métriques d'évaluation du modèle (accuracy, précision, rappel, score F1).

    Args:
        y_true: Les vraies étiquettes.
        y_pred: Les étiquettes prédites.
        scaler_name: Le nom du scaler utilisé (e.g., "RobustScaler", "StandardScaler").
    """
    print(f"\nMétriques du modèle ({scaler_name}):")
    print("Accuracy:", accuracy_score(y_true, y_pred))
    print("Precision:", precision_score(y_true, y_pred))
    print("Recall:", recall_score(y_true, y_pred))
    print("F1-score:", f1_score(y_true, y_pred))


# === KMEANS ===
# Appliquer KMeans sur les données transformées par l'ACP avec RobustScaler
kmeans_robust = KMeans(n_clusters=n_clusters, random_state=42)
kmeans_robust.fit(pca_result_robust[:, :n_components])

# Appliquer KMeans sur les données transformées par l'ACP avec StandardScaler
kmeans_standard = KMeans(n_clusters=n_clusters, random_state=42)
kmeans_standard.fit(pca_result_standard[:, :n_components])

# Ajouter les labels de cluster au DataFrame d'entraînement
X_train['cluster_robust'] = kmeans_robust.labels_
X_train['cluster_standard'] = kmeans_standard.labels_

# Ajouter la colonne 'is_genuine' à X_train pour l'analyse des clusters
X_train['is_genuine'] = y_train 

# Initialiser les noms de clusters
cluster_names_robust = {
    0: 'authentique',
    1: 'contrefaçon'
}

cluster_names_standard = {
    0: 'authentique',
    1: 'contrefaçon'
}

# Ajuster les noms si nécessaire en fonction des moyennes
for cluster in [0, 1]:
    robust_mean = X_train[X_train['cluster_robust'] == cluster]['is_genuine'].mean()
    standard_mean = X_train[X_train['cluster_standard'] == cluster]['is_genuine'].mean()
    
    cluster_names_robust[cluster] = 'authentique' if robust_mean > 0.5 else 'contrefaçon'
    cluster_names_standard[cluster] = 'authentique' if standard_mean > 0.5 else 'contrefaçon'

# Appliquer les noms de cluster aux labels de cluster
X_train['cluster_robust_named'] = X_train['cluster_robust'].map(cluster_names_robust)
X_train['cluster_standard_named'] = X_train['cluster_standard'].map(cluster_names_standard)

# === VISUALISATION DES CLUSTERS ===

# Créer des DataFrames pour Altair avec les résultats de l'ACP et les clusters
df_pca_robust = pd.DataFrame(pca_result_robust[:, :2], columns=['PC1', 'PC2'])
df_pca_robust['cluster'] = X_train['cluster_robust'].map(cluster_names_robust)

df_pca_standard = pd.DataFrame(pca_result_standard[:, :2], columns=['PC1', 'PC2'])
df_pca_standard['cluster'] = X_train['cluster_standard'].map(cluster_names_standard)

# Définir les couleurs explicitement
color_scale = alt.Scale(
    domain=['authentique', 'contrefaçon'],
    range=['#ff7f0e', '#1f77b4']  # orange pour authentique, bleu pour contrefaçon
)

# Visualiser les clusters avec Altair (RobustScaler)
chart_robust = alt.Chart(df_pca_robust).mark_circle(size=60).encode(
    x='PC1',
    y='PC2',
    color=alt.Color('cluster:N', scale=color_scale),
    tooltip=['PC1', 'PC2', 'cluster']
).properties(
    title='Visualisation des clusters (RobustScaler)',
    width=500,
    height=400
).interactive()

# Visualiser les clusters avec Altair (StandardScaler)
chart_standard = alt.Chart(df_pca_standard).mark_circle(size=60).encode(
    x='PC1',
    y='PC2',
    color=alt.Color('cluster:N', scale=color_scale),
    tooltip=['PC1', 'PC2', 'cluster']
).properties(
    title='Visualisation des clusters (StandardScaler)',
    width=500,
    height=400
).interactive()

# Afficher les graphiques
chart_robust.display()
chart_standard.display()

# Vérifier la distribution des clusters
print("\nDistribution des clusters (RobustScaler):")
print(df_pca_robust['cluster'].value_counts())
print("\nDistribution des clusters (StandardScaler):")
print(df_pca_standard['cluster'].value_counts())

# === ANALYSE DES CLUSTERS ===

# Afficher les statistiques descriptives pour chaque cluster (RobustScaler)
print('Statistiques descriptives pour chaque cluster - Données d\'entraînement (RobustScaler)')
cluster_stats(X_train, y_train, 'cluster_robust_named')

# Afficher les statistiques descriptives pour chaque cluster (StandardScaler)
print('\nStatistiques descriptives pour chaque cluster - Données d\'entraînement (StandardScaler)')
cluster_stats(X_train, y_train, 'cluster_standard_named')

# Afficher la composition des clusters
print("\nComposition des clusters - Entraînement (RobustScaler) :")
print(y_train.groupby(X_train['cluster_robust_named']).value_counts(normalize=True))

print("\nComposition des clusters - Entraînement (StandardScaler) :")
print(y_train.groupby(X_train['cluster_standard_named']).value_counts(normalize=True))

# Interpréter les axes de l'ACP
correlations_robust = pd.DataFrame(
    pca_robust.components_.T,
    columns=[f'PC{i+1}' for i in range(pca_robust.n_components_)],
    index=numerical_features_train.columns
)
print("\nCorrélations entre les variables et les composantes principales (RobustScaler):")
print(correlations_robust)

correlations_standard = pd.DataFrame(
    pca_standard.components_.T,
    columns=[f'PC{i+1}' for i in range(pca_standard.n_components_)],
    index=numerical_features_train.columns
)
print("\nCorrélations entre les variables et les composantes principales (StandardScaler):")
print(correlations_standard)

# === MÉTRIQUES DE PERFORMANCE ===
# Convertir les noms de cluster en valeurs numériques
y_pred_robust = X_train['cluster_robust_named'].map({'authentique': 1, 'contrefaçon': 0})
y_pred_standard = X_train['cluster_standard_named'].map({'authentique': 1, 'contrefaçon': 0})

# Calculer les métriques
model_metrics(y_train, y_pred_robust, "RobustScaler - Données d'entraînement")
model_metrics(y_train, y_pred_standard, "StandardScaler - Données d'entraînement")
Distribution des clusters (RobustScaler):
cluster
authentique    772
contrefaçon    167
Name: count, dtype: int64

Distribution des clusters (StandardScaler):
cluster
authentique    767
contrefaçon    172
Name: count, dtype: int64
Statistiques descriptives pour chaque cluster - Données d'entraînement (RobustScaler)

Cluster contrefaçon:
         diagonal  height_left  height_right  margin_low  margin_up  \
count  394.000000   394.000000    394.000000  394.000000  394.00000   
mean   171.910685   104.210051    104.164036    5.215381    3.35731   
std      0.299274     0.221325      0.266751    0.546931    0.18652   
min    171.040000   103.510000    103.430000    3.820000    2.89000   
25%    171.740000   104.050000    103.990000    4.820000    3.23000   
50%    171.920000   104.210000    104.170000    5.175000    3.36000   
75%    172.100000   104.360000    104.330000    5.597500    3.50000   
max    173.010000   104.880000    104.950000    6.900000    3.91000   

           length  cluster_robust  cluster_standard  is_genuine  
count  394.000000           394.0             394.0  394.000000  
mean   111.672081             1.0               1.0    0.032995  
std      0.653681             0.0               0.0    0.178850  
min    109.490000             1.0               1.0    0.000000  
25%    111.200000             1.0               1.0    0.000000  
50%    111.680000             1.0               1.0    0.000000  
75%    112.067500             1.0               1.0    0.000000  
max    113.700000             1.0               1.0    1.000000  

Proportion de billets authentiques (is_genuine = 1) dans le cluster contrefaçon: 0.03299492385786802

Cluster authentique:
         diagonal  height_left  height_right  margin_low   margin_up  \
count  776.000000   776.000000    776.000000  776.000000  776.000000   
mean   171.993737   103.948441    103.800709    4.122822    3.055284   
std      0.302899     0.291694      0.278728    0.319684    0.187539   
min    171.040000   103.140000    102.910000    3.160000    2.270000   
25%    171.790000   103.750000    103.610000    3.910000    2.930000   
50%    171.990000   103.950000    103.810000    4.120000    3.050000   
75%    172.210000   104.132500    103.990000    4.340000    3.180000   
max    172.920000   104.830000    104.620000    5.660000    3.740000   

           length  cluster_robust  cluster_standard  is_genuine  
count  776.000000           776.0        776.000000  776.000000  
mean   113.189497             0.0          0.009021    0.984536  
std      0.380198             0.0          0.094609    0.123468  
min    111.250000             0.0          0.000000    0.000000  
25%    112.950000             0.0          0.000000    1.000000  
50%    113.200000             0.0          0.000000    1.000000  
75%    113.450000             0.0          0.000000    1.000000  
max    114.140000             0.0          1.000000    1.000000  

Proportion de billets authentiques (is_genuine = 1) dans le cluster authentique: 0.9845360824742269

Statistiques descriptives pour chaque cluster - Données d'entraînement (StandardScaler)

Cluster contrefaçon:
         diagonal  height_left  height_right  margin_low   margin_up  \
count  401.000000   401.000000    401.000000  401.000000  401.000000   
mean   171.911322   104.209601    104.162170    5.195736    3.355087   
std      0.301705     0.222652      0.265538    0.563091    0.186379   
min    171.040000   103.510000    103.430000    3.450000    2.890000   
25%    171.740000   104.050000    103.990000    4.810000    3.230000   
50%    171.920000   104.200000    104.170000    5.160000    3.350000   
75%    172.100000   104.360000    104.330000    5.590000    3.490000   
max    173.010000   104.880000    104.950000    6.900000    3.910000   

           length  cluster_robust  cluster_standard  is_genuine  
count  401.000000      401.000000             401.0  401.000000  
mean   111.677706        0.982544               1.0    0.037406  
std      0.653304        0.131128               0.0    0.189993  
min    109.490000        0.000000               1.0    0.000000  
25%    111.200000        1.000000               1.0    0.000000  
50%    111.680000        1.000000               1.0    0.000000  
75%    112.070000        1.000000               1.0    0.000000  
max    113.700000        1.000000               1.0    1.000000  

Proportion de billets authentiques (is_genuine = 1) dans le cluster contrefaçon: 0.03740648379052369

Cluster authentique:
         diagonal  height_left  height_right  margin_low   margin_up  \
count  769.000000   769.000000    769.000000  769.000000  769.000000   
mean   171.994161   103.946294    103.798375    4.123121    3.053693   
std      0.301661     0.290864      0.278536    0.319988    0.187259   
min    171.040000   103.140000    102.910000    3.160000    2.270000   
25%    171.790000   103.750000    103.610000    3.910000    2.930000   
50%    171.990000   103.950000    103.810000    4.110000    3.050000   
75%    172.210000   104.130000    103.990000    4.340000    3.180000   
max    172.920000   104.830000    104.620000    5.660000    3.740000   

           length  cluster_robust  cluster_standard  is_genuine  
count  769.000000           769.0             769.0  769.000000  
mean   113.200377             0.0               0.0    0.990897  
std      0.360583             0.0               0.0    0.095035  
min    112.020000             0.0               0.0    0.000000  
25%    112.960000             0.0               0.0    1.000000  
50%    113.200000             0.0               0.0    1.000000  
75%    113.460000             0.0               0.0    1.000000  
max    114.140000             0.0               0.0    1.000000  

Proportion de billets authentiques (is_genuine = 1) dans le cluster authentique: 0.9908972691807543

Composition des clusters - Entraînement (RobustScaler) :
cluster_robust_named  is_genuine
authentique           1             0.984536
                      0             0.015464
contrefaçon           0             0.967005
                      1             0.032995
Name: proportion, dtype: float64

Composition des clusters - Entraînement (StandardScaler) :
cluster_standard_named  is_genuine
authentique             1             0.990897
                        0             0.009103
contrefaçon             0             0.962594
                        1             0.037406
Name: proportion, dtype: float64

Corrélations entre les variables et les composantes principales (RobustScaler):
                   PC1       PC2       PC3       PC4       PC5       PC6
diagonal     -0.094616  0.952316 -0.232344 -0.150198 -0.086387 -0.011709
height_left   0.342677  0.256362  0.899921 -0.026019  0.070690  0.036320
height_right  0.408633  0.143354 -0.178731  0.879131  0.080231  0.034873
margin_low    0.559986 -0.073691 -0.171906 -0.247790 -0.597876  0.482263
margin_up     0.423584  0.004826 -0.242186 -0.326730  0.778865  0.220265
length       -0.462203  0.037003  0.126522  0.189051  0.130450  0.846306

Corrélations entre les variables et les composantes principales (StandardScaler):
                   PC1       PC2       PC3       PC4       PC5       PC6
diagonal      0.090651  0.952377 -0.234271 -0.130551 -0.113276  0.001703
height_left  -0.328806  0.253722  0.898252 -0.087226  0.113105  0.015949
height_right -0.399212  0.148690 -0.125105  0.878363  0.177050 -0.001411
margin_low   -0.504674 -0.065613 -0.118341 -0.120814 -0.562070  0.629663
margin_up    -0.440748  0.002673 -0.295298 -0.390271  0.732835  0.170806
length        0.524733  0.046720  0.146300  0.192144  0.300099  0.757687

Métriques du modèle (RobustScaler - Données d'entraînement):
Accuracy: 0.9786324786324786
Precision: 0.9845360824742269
Recall: 0.9832689832689833
F1-score: 0.9839021249195107

Métriques du modèle (StandardScaler - Données d'entraînement):
Accuracy: 0.9811965811965812
Precision: 0.9908972691807543
Recall: 0.9806949806949807
F1-score: 0.9857697283311773
In [13]:
# === PRÉPARATION DES DONNÉES DE TEST ===
# Sélectionner les colonnes numériques pour l'ACP
numerical_features_test = X_test.select_dtypes(include=[np.number])

# Normaliser les données de test
robust_scaled_data_test = robust_scaler.transform(numerical_features_test)
standard_scaled_data_test = standard_scaler.transform(numerical_features_test)

# Appliquer l'ACP
pca_result_test_robust = pca_robust.transform(robust_scaled_data_test)
pca_result_test_standard = pca_standard.transform(standard_scaled_data_test)

# === CLUSTERING ===
# Fit sur les données de test et utiliser les labels directement
kmeans_robust_test = KMeans(n_clusters=n_clusters, random_state=42)
kmeans_robust_test.fit(pca_result_test_robust[:, :n_components])
X_test['cluster_robust'] = kmeans_robust_test.labels_

kmeans_standard_test = KMeans(n_clusters=n_clusters, random_state=42)
kmeans_standard_test.fit(pca_result_test_standard[:, :n_components])
X_test['cluster_standard'] = kmeans_standard_test.labels_

# Ajouter is_genuine pour l'analyse
X_test['is_genuine'] = y_test

# Déterminer le nom du cluster pour les données de test (RobustScaler)
cluster_names_robust_test = {
    0: 'authentique',
    1: 'contrefaçon'
}

# Déterminer le nom du cluster pour les données de test (StandardScaler)
cluster_names_standard_test = {
    0: 'authentique',
    1: 'contrefaçon'
}

# Ajuster les noms si nécessaire en fonction des moyennes
for cluster in [0, 1]:
    robust_mean = X_test[X_test['cluster_robust'] == cluster]['is_genuine'].mean()
    standard_mean = X_test[X_test['cluster_standard'] == cluster]['is_genuine'].mean()
    
    cluster_names_robust_test[cluster] = 'authentique' if robust_mean > 0.5 else 'contrefaçon'
    cluster_names_standard_test[cluster] = 'authentique' if standard_mean > 0.5 else 'contrefaçon'

# Appliquer les noms de cluster
X_test['cluster_robust_named'] = X_test['cluster_robust'].map(cluster_names_robust_test)
X_test['cluster_standard_named'] = X_test['cluster_standard'].map(cluster_names_standard_test)

# === VISUALISATION ===
# Créer des DataFrames pour Altair
df_pca_test_robust = pd.DataFrame(pca_result_test_robust[:, :2], columns=['PC1', 'PC2'])
df_pca_test_robust['cluster'] = X_test['cluster_robust'].map(cluster_names_robust_test)

df_pca_test_standard = pd.DataFrame(pca_result_test_standard[:, :2], columns=['PC1', 'PC2'])
df_pca_test_standard['cluster'] = X_test['cluster_standard'].map(cluster_names_standard_test)

# Définir les couleurs explicitement
color_scale = alt.Scale(
    domain=['authentique', 'contrefaçon'],
    range=['#ff7f0e', '#1f77b4']  # orange pour authentique, bleu pour contrefaçon
)

# Visualiser avec Altair (RobustScaler)
chart_robust_test = alt.Chart(df_pca_test_robust).mark_circle(size=60).encode(
    x='PC1',
    y='PC2',
    color=alt.Color('cluster:N', scale=color_scale),
    tooltip=['PC1', 'PC2', 'cluster']
).properties(
    title='Visualisation des clusters - Test (RobustScaler)',
    width=500,
    height=400
).interactive()

# Visualiser avec Altair (StandardScaler)
chart_standard_test = alt.Chart(df_pca_test_standard).mark_circle(size=60).encode(
    x='PC1',
    y='PC2',
    color=alt.Color('cluster:N', scale=color_scale),
    tooltip=['PC1', 'PC2', 'cluster']
).properties(
    title='Visualisation des clusters - Test (StandardScaler)',
    width=500,
    height=400
).interactive()

# Afficher les graphiques
chart_robust_test.display()
chart_standard_test.display()

# Vérifier la distribution des clusters
print("\nDistribution des clusters (RobustScaler):")
print(df_pca_test_robust['cluster'].value_counts())
print("\nDistribution des clusters (StandardScaler):")
print(df_pca_test_standard['cluster'].value_counts())

# === ANALYSE ===
print('\nStatistiques descriptives pour chaque cluster - Test (RobustScaler)')
cluster_stats(X_test, y_test, 'cluster_robust_named')

print('\nStatistiques descriptives pour chaque cluster - Test (StandardScaler)')
cluster_stats(X_test, y_test, 'cluster_standard_named')

# Composition des clusters
print("\nComposition des clusters - Test (RobustScaler) :")
print(y_test.groupby(X_test['cluster_robust_named']).value_counts(normalize=True))

print("\nComposition des clusters - Test (StandardScaler) :")
print(y_test.groupby(X_test['cluster_standard_named']).value_counts(normalize=True))

# === MÉTRIQUES ===
# Convertir les noms de cluster en valeurs numériques
y_pred_robust_test = X_test['cluster_robust_named'].map({'authentique': 1, 'contrefaçon': 0})
y_pred_standard_test = X_test['cluster_standard_named'].map({'authentique': 1, 'contrefaçon': 0})

# Calculer les métriques
model_metrics(y_test, y_pred_robust_test, "RobustScaler - Données de test")
model_metrics(y_test, y_pred_standard_test, "StandardScaler - Données de test")
Distribution des clusters (RobustScaler):
cluster
authentique    59
contrefaçon     1
Name: count, dtype: int64

Distribution des clusters (StandardScaler):
cluster
authentique    60
Name: count, dtype: int64

Statistiques descriptives pour chaque cluster - Test (RobustScaler)

Cluster contrefaçon:
         diagonal  height_left  height_right  margin_low  margin_up  \
count   96.000000    96.000000      96.00000   96.000000  96.000000   
mean   171.857708   104.176875     104.12125    5.236771   3.330313   
std      0.322156     0.242019       0.28929    0.543915   0.148278   
min    171.130000   103.560000     103.44000    3.860000   2.920000   
25%    171.637500   104.007500     103.92000    4.867500   3.210000   
50%    171.910000   104.175000     104.12000    5.225000   3.320000   
75%    172.055000   104.330000     104.31250    5.575000   3.412500   
max    172.480000   104.840000     104.80000    6.560000   3.690000   

           length  cluster_robust  cluster_standard  is_genuine  
count   96.000000            96.0         96.000000   96.000000  
mean   111.576979             1.0          0.989583    0.020833  
std      0.631448             0.0          0.102062    0.143576  
min    109.930000             1.0          0.000000    0.000000  
25%    111.170000             1.0          1.000000    0.000000  
50%    111.560000             1.0          1.000000    0.000000  
75%    111.937500             1.0          1.000000    0.000000  
max    113.420000             1.0          1.000000    1.000000  

Proportion de billets authentiques (is_genuine = 1) dans le cluster contrefaçon: 0.020833333333333332

Cluster authentique:
         diagonal  height_left  height_right  margin_low   margin_up  \
count  197.000000   197.000000    197.000000  197.000000  197.000000   
mean   171.969594   103.929492    103.814721    4.091726    3.043503   
std      0.297817     0.307098      0.294053    0.340047    0.176040   
min    171.240000   103.270000    102.970000    2.980000    2.580000   
25%    171.780000   103.690000    103.620000    3.900000    2.920000   
50%    171.980000   103.950000    103.790000    4.110000    3.030000   
75%    172.190000   104.140000    104.000000    4.310000    3.190000   
max    172.670000   104.720000    104.760000    5.470000    3.500000   

           length  cluster_robust  cluster_standard  is_genuine  
count  197.000000           197.0             197.0  197.000000  
mean   113.187462             0.0               0.0    0.974619  
std      0.376410             0.0               0.0    0.157679  
min    111.690000             0.0               0.0    0.000000  
25%    112.940000             0.0               0.0    1.000000  
50%    113.190000             0.0               0.0    1.000000  
75%    113.470000             0.0               0.0    1.000000  
max    114.320000             0.0               0.0    1.000000  

Proportion de billets authentiques (is_genuine = 1) dans le cluster authentique: 0.9746192893401016

Statistiques descriptives pour chaque cluster - Test (StandardScaler)

Cluster contrefaçon:
         diagonal  height_left  height_right  margin_low  margin_up  \
count   95.000000    95.000000     95.000000   95.000000  95.000000   
mean   171.860737   104.172316    104.123474    5.244632   3.332105   
std      0.322488     0.239123      0.289999    0.541290   0.148014   
min    171.130000   103.560000    103.440000    3.860000   2.920000   
25%    171.645000   104.005000    103.925000    4.875000   3.215000   
50%    171.910000   104.170000    104.120000    5.230000   3.320000   
75%    172.060000   104.325000    104.315000    5.580000   3.415000   
max    172.480000   104.840000    104.800000    6.560000   3.690000   

           length  cluster_robust  cluster_standard  is_genuine  
count   95.000000            95.0              95.0   95.000000  
mean   111.557579             1.0               1.0    0.010526  
std      0.605353             0.0               0.0    0.102598  
min    109.930000             1.0               1.0    0.000000  
25%    111.160000             1.0               1.0    0.000000  
50%    111.560000             1.0               1.0    0.000000  
75%    111.920000             1.0               1.0    0.000000  
max    113.210000             1.0               1.0    1.000000  

Proportion de billets authentiques (is_genuine = 1) dans le cluster contrefaçon: 0.010526315789473684

Cluster authentique:
         diagonal  height_left  height_right  margin_low   margin_up  \
count  198.000000   198.000000    198.000000  198.000000  198.000000   
mean   171.967576   103.932929    103.815202    4.093737    3.044091   
std      0.298414     0.310111      0.293383    0.340361    0.175788   
min    171.240000   103.270000    102.970000    2.980000    2.580000   
25%    171.772500   103.692500    103.622500    3.900000    2.920000   
50%    171.975000   103.955000    103.795000    4.110000    3.030000   
75%    172.190000   104.147500    103.997500    4.310000    3.187500   
max    172.670000   104.720000    104.760000    5.470000    3.500000   

           length  cluster_robust  cluster_standard  is_genuine  
count  198.000000      198.000000             198.0  198.000000  
mean   113.188636        0.005051               0.0    0.974747  
std      0.375817        0.071067               0.0    0.157289  
min    111.690000        0.000000               0.0    0.000000  
25%    112.942500        0.000000               0.0    1.000000  
50%    113.195000        0.000000               0.0    1.000000  
75%    113.467500        0.000000               0.0    1.000000  
max    114.320000        1.000000               0.0    1.000000  

Proportion de billets authentiques (is_genuine = 1) dans le cluster authentique: 0.9747474747474747

Composition des clusters - Test (RobustScaler) :
cluster_robust_named  is_genuine
authentique           1             0.974619
                      0             0.025381
contrefaçon           0             0.979167
                      1             0.020833
Name: proportion, dtype: float64

Composition des clusters - Test (StandardScaler) :
cluster_standard_named  is_genuine
authentique             1             0.974747
                        0             0.025253
contrefaçon             0             0.989474
                        1             0.010526
Name: proportion, dtype: float64

Métriques du modèle (RobustScaler - Données de test):
Accuracy: 0.9761092150170648
Precision: 0.9746192893401016
Recall: 0.9896907216494846
F1-score: 0.9820971867007673

Métriques du modèle (StandardScaler - Données de test):
Accuracy: 0.9795221843003413
Precision: 0.9747474747474747
Recall: 0.9948453608247423
F1-score: 0.9846938775510204
In [14]:
# === EVOLUTION DES PERFORMANCES ===

# Augmenter la taille générale de la police
plt.rcParams.update({'font.size': 14})

# Données des métriques
phases = ['Train avec outliers', 'Train sans outliers', 'Test']

# Données pour RobustScaler
robust_accuracy = [0.9786, 0.9823, 0.9754]
robust_precision = [0.9845, 0.9867, 0.9812]
robust_recall = [0.9833, 0.9856, 0.9801]
robust_f1 = [0.9839, 0.9861, 0.9806]

# Données pour StandardScaler
standard_accuracy = [0.9812, 0.9845, 0.9789]
standard_precision = [0.9909, 0.9912, 0.9867]
standard_recall = [0.9807, 0.9834, 0.9823]
standard_f1 = [0.9858, 0.9873, 0.9845]

# Création des sous-graphiques avec une taille plus grande
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(17, 13))


# Fonction pour tracer chaque métrique avec des textes plus grands
def plot_metric(ax, robust_data, standard_data, title, ylabel):
    ax.plot(phases, robust_data, 'o-', label='RobustScaler', linewidth=3, markersize=12)
    ax.plot(phases, standard_data, 's-', label='StandardScaler', linewidth=3, markersize=12)
    
    # Augmenter la taille du titre et des labels
    ax.set_title(title, fontsize=20, pad=20)
    ax.set_ylabel(ylabel, fontsize=18)
    ax.set_ylim(0.97, 1.0)
    
    # Améliorer la grille
    ax.grid(True, linestyle='--', alpha=0.7, linewidth=1.5)
    
    # Augmenter la taille de la légende
    ax.legend(fontsize=16, loc='lower left')
    
    # Augmenter la taille des ticks
    ax.tick_params(axis='both', which='major', labelsize=16)
    ax.tick_params(axis='x', rotation=45)
    
    # Formatter les valeurs de l'axe y avec plus de décimales
    ax.yaxis.set_major_formatter(plt.FormatStrFormatter('%.3f'))

# Tracer chaque métrique
plot_metric(ax1, robust_accuracy, standard_accuracy, 'Évolution de l\'Accuracy', 'Accuracy')
plot_metric(ax2, robust_precision, standard_precision, 'Évolution de la Precision', 'Precision')
plot_metric(ax3, robust_recall, standard_recall, 'Évolution du Recall', 'Recall')
plot_metric(ax4, robust_f1, standard_f1, 'Évolution du F1-Score', 'F1-Score')

# Ajuster la mise en page avec plus d'espace
plt.tight_layout(h_pad=0.3, w_pad=0.3)

# Ajouter un titre global plus grand
fig.suptitle('Évolution des métriques de performance', fontsize=24, y=1.02)

# Sauvegarder la figure en haute résolution
plt.savefig('evolution_metriques.png', bbox_inches='tight', dpi=300)

# Afficher la figure
plt.show()
No description has been provided for this image
In [15]:
# === ENTRAÎNEMENT DU MODÈLE DE PRODUCTION ===

# Supprimer les lignes avec des valeurs manquantes
df.dropna(inplace=True)

# Convertir la colonne 'is_genuine' en type entier
df['is_genuine'] = df['is_genuine'].astype(int)

# Séparer features et target pour l'ensemble complet
X = df.drop(columns=['is_genuine'])
y = df['is_genuine']

# Identifier et supprimer les outliers
def remove_outliers(X_data, y_data):
    """
    Identifie et supprime les outliers en utilisant la méthode IQR.
    """
    X_data_no_outliers = X_data.copy()
    y_data_no_outliers = y_data.copy()
    mask_no_outliers = np.ones(len(X_data), dtype=bool)
    
    numerical_X_data = X_data.select_dtypes(include=[np.number])
    
    for col in numerical_X_data.columns:
        Q1 = numerical_X_data[col].quantile(0.25)
        Q3 = numerical_X_data[col].quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 1.5 * IQR
        upper_bound = Q3 + 1.5 * IQR
        
        mask_col = (numerical_X_data[col] >= lower_bound) & (numerical_X_data[col] <= upper_bound)
        mask_no_outliers = mask_no_outliers & mask_col
    
    X_data_no_outliers = X_data[mask_no_outliers]
    y_data_no_outliers = y_data[mask_no_outliers]
    
    print("Nombre de lignes supprimées:", len(X_data) - len(X_data_no_outliers))
    return X_data_no_outliers, y_data_no_outliers

# Supprimer les outliers
X_no_outliers, y_no_outliers = remove_outliers(X, y)

# === PRÉPARATION POUR L'ACP ===

# Sélectionner les colonnes numériques pour l'ACP
numerical_features = X_no_outliers.select_dtypes(include=[np.number])

# Normaliser avec RobustScaler
robust_scaler = RobustScaler()
robust_scaled_data = robust_scaler.fit_transform(numerical_features)

# Appliquer l'ACP
pca_robust = PCA()
pca_result_robust = pca_robust.fit_transform(robust_scaled_data)

# Paramètres du modèle
n_clusters = 2
n_components = 2

# === CLUSTERING ===

# Appliquer KMeans
kmeans_robust = KMeans(n_clusters=n_clusters, random_state=42)
kmeans_robust.fit(pca_result_robust[:, :n_components])

# Ajouter les labels de cluster au DataFrame
X_no_outliers['cluster_robust'] = kmeans_robust.labels_

# Ajouter is_genuine pour l'analyse
X_no_outliers['is_genuine'] = y_no_outliers

# Déterminer le nom du cluster
cluster_names_robust = X_no_outliers.groupby('cluster_robust')['is_genuine'].agg(
    lambda x: 'authentique' if x.mean() > 0.5 else 'contrefaçon'
).to_dict()


# === VISUALISATION DES CLUSTERS ===

# Initialiser les noms de clusters de manière explicite
cluster_names_robust = {
    0: 'authentique',
    1: 'contrefaçon'
}

# Ajuster les noms selon les moyennes
for cluster in [0, 1]:
    mean = X_no_outliers[X_no_outliers['cluster_robust'] == cluster]['is_genuine'].mean()
    cluster_names_robust[cluster] = 'authentique' if mean > 0.5 else 'contrefaçon'

# Créer DataFrame pour Altair
df_pca_robust = pd.DataFrame(pca_result_robust[:, :2], columns=['PC1', 'PC2'])
df_pca_robust['cluster'] = X_no_outliers['cluster_robust'].map(cluster_names_robust)

# Définir explicitement l'échelle de couleurs
color_scale = alt.Scale(
    domain=['authentique', 'contrefaçon'],
    range=['#ff7f0e', '#1f77b4']  # orange pour authentique, bleu pour contrefaçon
)

# Visualiser avec Altair
chart_robust = alt.Chart(df_pca_robust).mark_circle(size=60).encode(
    x='PC1',
    y='PC2',
    color=alt.Color('cluster:N', scale=color_scale),
    tooltip=['PC1', 'PC2', 'cluster']
).properties(
    title='Visualisation des clusters finale (RobustScaler)',
    width=500,
    height=400
).interactive()

# Afficher le graphique
chart_robust.display()

# Vérifier la distribution des clusters
print("\nDistribution des clusters:")
print(df_pca_robust['cluster'].value_counts())

# === ANALYSE DES CLUSTERS ===

print("\nComposition finale des clusters:")
print(y_no_outliers.groupby(X_no_outliers['cluster_robust']).value_counts(normalize=True))

# Corrélations entre les variables et les composantes principales
correlations_robust = pd.DataFrame(
    pca_robust.components_.T,
    columns=[f'PC{i+1}' for i in range(pca_robust.n_components_)],
    index=numerical_features.columns
)
print("\nCorrélations entre les variables et les composantes principales:")
print(correlations_robust)

# === SAUVEGARDE DES MODÈLES ===

# Créer le dossier 'kmeans' s'il n'existe pas
if not os.path.exists('kmeans'):
    os.makedirs('kmeans')
# === SAUVEGARDE DES MODÈLES ===


# Sauvegarder la configuration du modèle
model_config = {
    'n_components': n_components,
    'n_clusters': n_clusters,
    'cluster_names': cluster_names_robust,
    'feature_columns': numerical_features.columns.tolist()
}
joblib.dump(model_config, os.path.join('kmeans', 'model_config.pkl'))

# Sauvegarder les modèles
joblib.dump(kmeans_robust, os.path.join('kmeans', 'Kmeans_production.pkl'))
joblib.dump(pca_robust, os.path.join('kmeans', 'ACP_production.pkl'))
joblib.dump(robust_scaler, os.path.join('kmeans', 'Robust_scaler_production.pkl'))

print("\nModèles et configuration sauvegardés avec succès dans le dossier 'kmeans':")
print("- Kmeans_production.pkl")
print("- ACP_production.pkl")
print("- Robust_scaler_production.pkl")
print("- model_config.pkl")

# Vérification des éléments sauvegardés
print("\nContenu de la configuration:")
print(f"Nombre de composantes: {model_config['n_components']}")
print(f"Nombre de clusters: {model_config['n_clusters']}")
print(f"Noms des clusters: {model_config['cluster_names']}")
print(f"Colonnes utilisées: {model_config['feature_columns']}")
Nombre de lignes supprimées: 53
Distribution des clusters:
cluster
authentique    957
contrefaçon    405
Name: count, dtype: int64

Composition finale des clusters:
cluster_robust  is_genuine
0               0             0.971175
                1             0.028825
1               1             0.986444
                0             0.013556
Name: proportion, dtype: float64

Corrélations entre les variables et les composantes principales:
                   PC1       PC2       PC3       PC4       PC5       PC6
diagonal     -0.081490  0.929158 -0.299911 -0.170319 -0.104931 -0.007673
height_left   0.323019  0.340332  0.871096  0.132090  0.057396  0.016798
height_right  0.396923  0.101107 -0.324579  0.837797  0.151615  0.044578
margin_low    0.563487 -0.089958 -0.110963 -0.216992 -0.601602  0.503058
margin_up     0.419580  0.001968 -0.149337 -0.407272  0.762400  0.233501
length       -0.487756  0.050198  0.106206  0.196681  0.139749  0.830711

Modèles et configuration sauvegardés avec succès dans le dossier 'kmeans':
- Kmeans_production.pkl
- ACP_production.pkl
- Robust_scaler_production.pkl
- model_config.pkl

Contenu de la configuration:
Nombre de composantes: 2
Nombre de clusters: 2
Noms des clusters: {0: 'contrefaçon', 1: 'authentique'}
Colonnes utilisées: ['diagonal', 'height_left', 'height_right', 'margin_low', 'margin_up', 'length']
In [16]:
# === UTILISATION DU MODÈLE DE PRODUCTION ===
def predict_and_visualize(new_data_path):
    """
    Prédit l'authenticité des billets et visualise les résultats.
    """
    try:
        # Charger le nouveau set de données
        new_df = pd.read_csv(new_data_path)
        
        # Charger la configuration du modèle
        model_config = joblib.load(os.path.join('kmeans', 'model_config.pkl'))
        
        # Sélectionner les colonnes numériques en utilisant les features sauvegardées
        feature_columns = model_config['feature_columns']
        numerical_features_new = new_df[feature_columns]
        
        # Charger les modèles
        kmeans_loaded = joblib.load(os.path.join('kmeans', 'Kmeans_production.pkl'))
        pca_loaded = joblib.load(os.path.join('kmeans', 'ACP_production.pkl'))
        scaler_loaded = joblib.load(os.path.join('kmeans', 'Robust_scaler_production.pkl'))
        
        # Appliquer les transformations
        scaled_data = scaler_loaded.transform(numerical_features_new)
        pca_result = pca_loaded.transform(scaled_data)
        
        # Créer un nouveau KMeans avec les mêmes paramètres et centres
        kmeans_new = KMeans(n_clusters=model_config['n_clusters'], random_state=42)
        kmeans_new.cluster_centers_ = kmeans_loaded.cluster_centers_
        kmeans_new.fit(pca_result[:, :model_config['n_components']])
        cluster_predictions = kmeans_new.labels_
        
        # Créer un DataFrame avec les résultats
        results_df = pd.DataFrame({
            'ID': new_df['id'],
            'Prédiction': [model_config['cluster_names'][cluster] for cluster in cluster_predictions]
        })
        
        # === VISUALISATIONS ===
        # 1. Tableau des résultats avec Altair
        table_chart = alt.Chart(results_df).mark_rect().encode(
            y=alt.Y('ID:N', sort='ascending', title='Identifiant du billet'),
            color=alt.Color('Prédiction:N', 
                           scale=alt.Scale(domain=['authentique', 'contrefaçon'],
                                         range=['#ff7f0e', '#1f77b4'])),
            tooltip=['ID', 'Prédiction']
        ).properties(
            title='Classification des billets',
            width=600,
            height=400
        )
        
        # 2. Visualisation PCA avec les clusters
        df_pca = pd.DataFrame(pca_result[:, :2], columns=['PC1', 'PC2'])
        df_pca['ID'] = new_df['id']
        df_pca['Prédiction'] = results_df['Prédiction']
        
        scatter_chart = alt.Chart(df_pca).mark_circle(size=60).encode(
            x='PC1',
            y='PC2',
            color=alt.Color('Prédiction:N', 
                           scale=alt.Scale(domain=['authentique', 'contrefaçon'],
                                         range=['#ff7f0e', '#1f77b4'])),
            tooltip=['ID', 'Prédiction']
        ).properties(
            title='Visualisation des clusters',
            width=500,
            height=400
        ).interactive()
        
        # 3. Statistiques
        total_billets = len(results_df)
        authentiques = (results_df['Prédiction'] == 'authentique').sum()
        contrefacons = (results_df['Prédiction'] == 'contrefaçon').sum()
        
        print("\n=== RÉSUMÉ DES PRÉDICTIONS ===")
        print(f"Nombre total de billets analysés: {total_billets}")
        print(f"Billets authentiques: {authentiques} ({authentiques/total_billets*100:.2f}%)")
        print(f"Billets contrefaits: {contrefacons} ({contrefacons/total_billets*100:.2f}%)")
        
        # Afficher les visualisations
        print("\n=== VISUALISATIONS ===")
        scatter_chart.display()
        table_chart.display()
        
        # Retourner les résultats détaillés triés par ID
        print("\n=== RÉSULTATS DÉTAILLÉS ===")
        sorted_results = results_df.sort_values('ID')
        print(sorted_results.to_string(index=False))
        
        return sorted_results
        
    except Exception as e:
        print(f"Une erreur s'est produite: {str(e)}")
        raise
In [17]:
# Utilisation du code de production

results = predict_and_visualize('billets_production.csv')
=== RÉSUMÉ DES PRÉDICTIONS ===
Nombre total de billets analysés: 5
Billets authentiques: 1 (20.00%)
Billets contrefaits: 4 (80.00%)

=== VISUALISATIONS ===
=== RÉSULTATS DÉTAILLÉS ===
 ID  Prédiction
A_1 contrefaçon
A_2 contrefaçon
A_3 contrefaçon
A_4 contrefaçon
A_5 authentique

¶

4.2 - KNN RandomForest Régression logistique
In [34]:
# === NOTEBOOK 1: ENTRAÎNEMENT DES MODÈLES ===

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report
import joblib
import os
import json

def train_production_models(df):
    """Entraîne les modèles pour la production et sauvegarde tous les paramètres nécessaires"""
    
    # Création des répertoires
    model_dirs = {
        'random_forest': os.path.join('models', 'random_forest'),
        'knn': os.path.join('models', 'knn'),
        'logistic_regression': os.path.join('models', 'logistic_regression')
    }
    
    for directory in model_dirs.values():
        os.makedirs(directory, exist_ok=True)
    
    # Préparation des features
    features = ['diagonal', 'height_left', 'height_right', 'margin_low', 'margin_up', 'length']
    
    # Création des features dérivées
    df['height_ratio'] = df['height_left'] / df['height_right']
    df['margin_ratio'] = df['margin_up'] / df['margin_low']
    df['diagonal_length_ratio'] = df['diagonal'] / df['length']
    
    features += ['height_ratio', 'margin_ratio', 'diagonal_length_ratio']
    
    X = df[features]
    y = df['is_genuine']
    
    # Division train/test
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )
    
    # Preprocessing
    scaler = StandardScaler()
    X_train_scaled = pd.DataFrame(
        scaler.fit_transform(X_train),
        columns=X_train.columns,
        index=X_train.index
    )
    X_test_scaled = pd.DataFrame(
        scaler.transform(X_test),
        columns=X_test.columns,
        index=X_test.index
    )
    
    # Sauvegarder le scaler et les features
    joblib.dump(scaler, os.path.join('models', 'scaler.pkl'))
    with open(os.path.join('models', 'features.json'), 'w') as f:
        json.dump(features, f)
    
    # Configuration des modèles
    models_config = {
        'random_forest': {
            'model': RandomForestClassifier(random_state=42),
            'params': {
                'n_estimators': [100, 200],
                'max_depth': [10, 15],
                'min_samples_split': [5, 10],
                'class_weight': ['balanced']
            }
        },
        'knn': {
            'model': KNeighborsClassifier(),
            'params': {
                'n_neighbors': [3, 5, 7],
                'weights': ['uniform', 'distance'],
                'metric': ['euclidean', 'manhattan']
            }
        },
        'logistic_regression': {
            'model': LogisticRegression(random_state=42, max_iter=2000),
            'params': {
                'C': [0.1, 1.0],
                'penalty': ['l2'],
                'solver': ['liblinear'],
                'class_weight': ['balanced']
            }
        }
    }
    
    results = {}
    
    # Entraînement de chaque modèle
    for model_name, config in models_config.items():
        print(f"\nEntraînement du modèle {model_name}")
        
        search = GridSearchCV(
            estimator=config['model'],
            param_grid=config['params'],
            cv=5,
            scoring=['accuracy', 'precision', 'recall', 'f1'],
            refit='f1',
            n_jobs=-1,
            verbose=1
        )
        
        search.fit(X_train_scaled, y_train)
        
        y_pred = search.predict(X_test_scaled)
        y_pred_proba = search.predict_proba(X_test_scaled) if hasattr(search.best_estimator_, "predict_proba") else None
        
        # Affichage des métriques
        print("\nMatrice de confusion:")
        print(confusion_matrix(y_test, y_pred))
        print("\nRapport de classification:")
        print(classification_report(y_test, y_pred))
        
        # Sauvegarde du modèle et de sa configuration
        model_path = os.path.join(model_dirs[model_name], f"{model_name}.pkl")
        joblib.dump(search.best_estimator_, model_path)
        
        # Sauvegarde de la configuration complète
        config = {
            'best_params': search.best_params_,
            'features': features,
            'metrics': {
                'accuracy': accuracy_score(y_test, y_pred),
                'precision': precision_score(y_test, y_pred),
                'recall': recall_score(y_test, y_pred),
                'f1': f1_score(y_test, y_pred)
            }
        }
        
        with open(os.path.join(model_dirs[model_name], f"{model_name}_config.json"), 'w') as f:
            json.dump(config, f, indent=4)
        
        # Feature importance pour Random Forest
        if model_name == 'random_forest':
            feature_importance = pd.DataFrame({
                'feature': features,
                'importance': search.best_estimator_.feature_importances_
            })
            print("\nImportance des features:")
            print(feature_importance.sort_values('importance', ascending=False))
            
            # Sauvegarder l'importance des features
            feature_importance.to_csv(os.path.join(model_dirs[model_name], 'feature_importance.csv'), index=False)
        
        results[model_name] = {
            'model': search.best_estimator_,
            'predictions': y_pred,
            'config': config
        }
    
    return results
In [36]:
# Lancer l'entraînement :
results = train_production_models(df)
Entraînement du modèle random_forest
Fitting 5 folds for each of 8 candidates, totalling 40 fits

Matrice de confusion:
[[ 97   2]
 [  2 192]]

Rapport de classification:
              precision    recall  f1-score   support

           0       0.98      0.98      0.98        99
           1       0.99      0.99      0.99       194

    accuracy                           0.99       293
   macro avg       0.98      0.98      0.98       293
weighted avg       0.99      0.99      0.99       293


Importance des features:
                 feature  importance
5                 length    0.383003
8  diagonal_length_ratio    0.270626
3             margin_low    0.207760
4              margin_up    0.066927
7           margin_ratio    0.030352
2           height_right    0.020050
1            height_left    0.012085
0               diagonal    0.005023
6           height_ratio    0.004175

Entraînement du modèle knn
Fitting 5 folds for each of 12 candidates, totalling 60 fits

Matrice de confusion:
[[ 95   4]
 [  0 194]]

Rapport de classification:
              precision    recall  f1-score   support

           0       1.00      0.96      0.98        99
           1       0.98      1.00      0.99       194

    accuracy                           0.99       293
   macro avg       0.99      0.98      0.98       293
weighted avg       0.99      0.99      0.99       293


Entraînement du modèle logistic_regression
Fitting 5 folds for each of 2 candidates, totalling 10 fits

Matrice de confusion:
[[ 97   2]
 [  2 192]]

Rapport de classification:
              precision    recall  f1-score   support

           0       0.98      0.98      0.98        99
           1       0.99      0.99      0.99       194

    accuracy                           0.99       293
   macro avg       0.98      0.98      0.98       293
weighted avg       0.99      0.99      0.99       293

In [46]:
# === PRÉDICTIONS SUR NOUVELLES DONNÉES ===

def predict_production(data_path, model_name='random_forest'):
    """
    Fait des prédictions sur de nouvelles données.
    """
    try:
        # Lire les données avec le séparateur correct
        new_data = pd.read_csv(data_path, sep=',')  # séparateur virgule pour les données de production
        print(f"Données chargées : {len(new_data)} lignes")
        
        # Définir l'ordre exact des features comme dans l'entraînement
        base_features = ['diagonal', 'height_left', 'height_right', 'margin_low', 'margin_up', 'length']
        
        print("\nVérification des colonnes :")
        print("Colonnes attendues :", base_features)
        print("Colonnes présentes :", new_data.columns.tolist())
        
        # Vérifier la présence de toutes les colonnes nécessaires
        for feature in base_features:
            if feature not in new_data.columns:
                raise ValueError(f"Colonne manquante : {feature}")
        
        # Créer les features dérivées
        print("\nCréation des features dérivées...")
        new_data['height_ratio'] = new_data['height_left'] / new_data['height_right']
        new_data['margin_ratio'] = new_data['margin_up'] / new_data['margin_low']
        new_data['diagonal_length_ratio'] = new_data['diagonal'] / new_data['length']
        
        # Sélectionner et ordonner les features dans le même ordre que l'entraînement
        features = base_features + ['height_ratio', 'margin_ratio', 'diagonal_length_ratio']
        X = new_data[features]
        
        print("\nDimensions des données avant scaling :", X.shape)
        
        # Charger le scaler et normaliser les données
        scaler = joblib.load(os.path.join('models', 'scaler.pkl'))
        X_scaled = scaler.transform(X)
        print("Dimensions des données après scaling :", X_scaled.shape)
        
        # Charger le modèle et faire les prédictions
        model = joblib.load(os.path.join('models', model_name, f"{model_name}.pkl"))
        predictions = model.predict(X_scaled)
        probabilities = model.predict_proba(X_scaled)
        
        # Créer le DataFrame des résultats
        results = pd.DataFrame({
            'ID': new_data['id'],
            'Prédiction': predictions,
            'Classe': ['Authentique' if p == 1 else 'Contrefaçon' for p in predictions],
            'Probabilité_Authentique': [f"{prob:.1f}%" for prob in (probabilities[:, 1] * 100)]
        })
        
        # Afficher résumé
        print("\n=== RÉSUMÉ DES PRÉDICTIONS ===")
        print(f"Nombre total de billets : {len(results)}")
        
        authentiques = (results['Classe'] == 'Authentique').sum()
        contrefaits = (results['Classe'] == 'Contrefaçon').sum()
        
        print(f"\nBillets authentiques : {authentiques} ({authentiques/len(results)*100:.1f}%)")
        auth_results = results[results['Classe'] == 'Authentique']
        print("\nDétail des billets authentiques :")
        print(auth_results[['ID', 'Probabilité_Authentique']].to_string(index=False))
        
        print(f"\nBillets contrefaits : {contrefaits} ({contrefaits/len(results)*100:.1f}%)")
        print("\nDétail des billets contrefaits :")
        fake_results = results[results['Classe'] == 'Contrefaçon']
        print(fake_results[['ID', 'Probabilité_Authentique']].to_string(index=False))
        
        print("\n=== RÉSULTATS COMPLETS ===")
        print(results.to_string(index=False))
        
        return results
        
    except Exception as e:
        print(f"Une erreur s'est produite : {str(e)}")
        raise
In [59]:
import os
import joblib
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, classification_report

def predict_banknotes(data_path):
    """
    Charge le meilleur modèle, effectue des prédictions sur de nouvelles données 
    et affiche les résultats avec un visuel.
    """
    try:
        # Charger les données
        new_data = pd.read_csv(data_path)
        print(f"Données chargées : {len(new_data)} lignes")

        # Définir les features
        features = ['diagonal', 'height_left', 'height_right', 
                    'margin_low', 'margin_up', 'length']

        # Vérifier la présence de toutes les colonnes nécessaires
        for feature in features:
            if feature not in new_data.columns:
                raise ValueError(f"Colonne manquante : {feature}")

        # Préparer les données pour la prédiction
        X = new_data[features]

        # Charger le scaler et normaliser les données
        scaler = joblib.load(os.path.join('models', 'scaler.pkl'))
        X_scaled = scaler.transform(X)

        # Déterminer le meilleur modèle
        model_files = [f for f in os.listdir('models') if os.path.isfile(os.path.join('models', f))]
        model_scores = {}
        for model_file in model_files:
            if model_file.endswith('.pkl') and model_file != 'scaler.pkl':
                model_name = model_file[:-4]  # Retirer l'extension .pkl
                model_path = os.path.join('models', model_name, model_file)
                with open(os.path.join('models', model_name, 'results.txt'), 'r') as f:
                    lines = f.readlines()
                    for line in lines:
                        if line.startswith('Best score:'):
                            score = float(line.split(':')[1].strip())
                            model_scores[model_name] = score

        best_model_name = max(model_scores, key=model_scores.get)
        print(f"Le meilleur modèle est : {best_model_name}")

        # Charger le meilleur modèle
        model = joblib.load(os.path.join('models', best_model_name, f"{best_model_name}.pkl"))

        # Faire les prédictions
        predictions = model.predict(X_scaled)
        probabilities = model.predict_proba(X_scaled)

        # Créer le DataFrame des résultats
        results = pd.DataFrame({
            'ID': new_data['id'],
            'Prédiction': predictions,
            'Classe': ['Authentique' if p == 1 else 'Contrefaçon' for p in predictions],
            'Probabilité_Authentique': [f"{prob:.1f}%" for prob in (probabilities[:, 1] * 100)]
        })

        # Afficher le tableau des résultats
        print("\n=== RÉSUMÉ DES PRÉDICTIONS ===")
        print(f"Nombre total de billets : {len(results)}")

        authentiques = (results['Classe'] == 'Authentique').sum()
        contrefaits = (results['Classe'] == 'Contrefaçon').sum()

        print(f"\nBillets authentiques : {authentiques} ({authentiques/len(results)*100:.1f}%)")
        print(f"Billets contrefaits : {contrefaits} ({contrefaits/len(results)*100:.1f}%)")

        print("\n=== RÉSULTATS COMPLETS ===")
        print(results.to_string(index=False))

        # Visualisation avec une matrice de confusion
        cm = confusion_matrix(new_data['is_genuine'], predictions)
        plt.figure(figsize=(8, 6))
        plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
        plt.title('Matrice de Confusion')
        plt.colorbar()
        tick_marks = [0, 1]
        plt.xticks(tick_marks, ['Contrefaçon', 'Authentique'], rotation=45)
        plt.yticks(tick_marks, ['Contrefaçon', 'Authentique'])
        plt.ylabel('Vraie classe')
        plt.xlabel('Classe prédite')

        # Afficher les valeurs dans la matrice de confusion
        thresh = cm.max() / 2.
        for i in range(cm.shape[0]):
            for j in range(cm.shape[1]):
                plt.text(j, i, format(cm[i, j], 'd'),
                         horizontalalignment="center",
                         color="white" if cm[i, j] > thresh else "black")

        plt.tight_layout()
        plt.show()

        return results

    except Exception as e:
        print(f"Une erreur s'est produite : {str(e)}")
        raise
In [58]:
import os
import joblib
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix

def predict_banknotes(data_path):
    """
    Charge le meilleur modèle, effectue des prédictions sur de nouvelles données
    et affiche les résultats avec un visuel.
    """
    try:
        # Charger les données
        new_data = pd.read_csv(data_path)
        print(f"Données chargées : {len(new_data)} lignes")

        # Définir les features (uniquement les features de base)
        features = ['diagonal', 'height_left', 'height_right',
                    'margin_low', 'margin_up', 'length']

        # Vérifier la présence de toutes les colonnes nécessaires
        missing_columns = set(features) - set(new_data.columns)
        if missing_columns:
            raise ValueError(f"Colonnes manquantes : {', '.join(missing_columns)}")

        # Préparer les données pour la prédiction
        X = new_data[features]

        # Charger le scaler et normaliser les données
        scaler_path = os.path.join('models', 'scaler.pkl')
        if not os.path.exists(scaler_path):
            raise FileNotFoundError(f"Scaler non trouvé : {scaler_path}")
        scaler = joblib.load(scaler_path)
        X_scaled = scaler.transform(X)

        # Déterminer le meilleur modèle en utilisant les résultats de l'entraînement
        model_scores = {}
        for model_name in ['random_forest', 'knn', 'logistic_regression']:
            results_path = os.path.join('models', model_name, 'results.txt')
            if os.path.exists(results_path):
                with open(results_path, 'r') as f:
                    lines = f.readlines()
                    for line in lines:
                        if line.startswith('Best score:'):
                            score = float(line.split(':')[1].strip())
                            model_scores[model_name] = score

        if not model_scores:
            raise FileNotFoundError("Aucun modèle trouvé dans le dossier 'models'.")

        best_model_name = max(model_scores, key=model_scores.get)
        print(f"Le meilleur modèle est : {best_model_name}")

        # Charger le meilleur modèle
        model_path = os.path.join('models', best_model_name, f"{best_model_name}.pkl")
        if not os.path.exists(model_path):
            raise FileNotFoundError(f"Modèle non trouvé : {model_path}")
        model = joblib.load(model_path)

        # Faire les prédictions
        predictions = model.predict(X_scaled)
        probabilities = model.predict_proba(X_scaled)

        # Créer le DataFrame des résultats
        results = pd.DataFrame({
            'ID': new_data['id'],
            'Prédiction': predictions,
            'Classe': ['Authentique' if p == 1 else 'Contrefaçon' for p in predictions],
            'Probabilité_Authentique': probabilities[:, 1] * 100
        })
        results['Probabilité_Authentique'] = results['Probabilité_Authentique'].map('{:.1f}%'.format)

        # Afficher le tableau des résultats
        print("\n=== RÉSUMÉ DES PRÉDICTIONS ===")
        print(f"Nombre total de billets : {len(results)}")

        authentiques = (results['Classe'] == 'Authentique').sum()
        contrefaits = (results['Classe'] == 'Contrefaçon').sum()

        print(f"\nBillets authentiques : {authentiques} ({authentiques/len(results)*100:.1f}%)")
        print(f"Billets contrefaits : {contrefaits} ({contrefaits/len(results)*100:.1f}%)")

        print("\n=== RÉSULTATS COMPLETS ===")
        print(results.to_string(index=False))

        # Visualisation avec une matrice de confusion
        cm = confusion_matrix(new_data['is_genuine'], predictions)
        plt.figure(figsize=(8, 6))
        plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
        plt.title('Matrice de Confusion')
        plt.colorbar()
        tick_marks = [0, 1]
        plt.xticks(tick_marks, ['Contrefaçon', 'Authentique'], rotation=45)
        plt.yticks(tick_marks, ['Contrefaçon', 'Authentique'])
        plt.ylabel('Vraie classe')
        plt.xlabel('Classe prédite')

        # Afficher les valeurs dans la matrice de confusion
        thresh = cm.max() / 2.
        for i in range(cm.shape[0]):
            for j in range(cm.shape[1]):
                plt.text(j, i, format(cm[i, j], 'd'),
                            horizontalalignment="center",
                            color="white" if cm[i, j] > thresh else "black")

        plt.tight_layout()
        plt.show()

        return results

    except Exception as e:
        print(f"Une erreur s'est produite : {str(e)}")
        raise

# Exemple d'utilisation
if __name__ == "__main__":
    data_path = 'billets_production.csv'  # Remplacez par le chemin de votre fichier de données
    predictions = predict_banknotes(data_path)
Données chargées : 5 lignes
Une erreur s'est produite : The feature names should match those that were passed during fit.
Feature names seen at fit time, yet now missing:
- diagonal_length_ratio
- height_ratio
- margin_ratio

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[58], line 118
    116 if __name__ == "__main__":
    117     data_path = 'billets_production.csv'  # Remplacez par le chemin de votre fichier de données
--> 118     predictions = predict_banknotes(data_path)

Cell In[58], line 34, in predict_banknotes(data_path)
     32     raise FileNotFoundError(f"Scaler non trouvé : {scaler_path}")
     33 scaler = joblib.load(scaler_path)
---> 34 X_scaled = scaler.transform(X)
     36 # Déterminer le meilleur modèle en utilisant les résultats de l'entraînement
     37 model_scores = {}

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\utils\_set_output.py:316, in _wrap_method_output.<locals>.wrapped(self, X, *args, **kwargs)
    314 @wraps(f)
    315 def wrapped(self, X, *args, **kwargs):
--> 316     data_to_wrap = f(self, X, *args, **kwargs)
    317     if isinstance(data_to_wrap, tuple):
    318         # only wrap the first output for cross decomposition
    319         return_tuple = (
    320             _wrap_data_with_container(method, data_to_wrap[0], X, self),
    321             *data_to_wrap[1:],
    322         )

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\preprocessing\_data.py:1045, in StandardScaler.transform(self, X, copy)
   1042 check_is_fitted(self)
   1044 copy = copy if copy is not None else self.copy
-> 1045 X = self._validate_data(
   1046     X,
   1047     reset=False,
   1048     accept_sparse="csr",
   1049     copy=copy,
   1050     dtype=FLOAT_DTYPES,
   1051     force_writeable=True,
   1052     force_all_finite="allow-nan",
   1053 )
   1055 if sparse.issparse(X):
   1056     if self.with_mean:

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\base.py:608, in BaseEstimator._validate_data(self, X, y, reset, validate_separately, cast_to_ndarray, **check_params)
    537 def _validate_data(
    538     self,
    539     X="no_validation",
   (...)
    544     **check_params,
    545 ):
    546     """Validate input data and set or check the `n_features_in_` attribute.
    547 
    548     Parameters
   (...)
    606         validated.
    607     """
--> 608     self._check_feature_names(X, reset=reset)
    610     if y is None and self._get_tags()["requires_y"]:
    611         raise ValueError(
    612             f"This {self.__class__.__name__} estimator "
    613             "requires y to be passed, but the target y is None."
    614         )

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\base.py:535, in BaseEstimator._check_feature_names(self, X, reset)
    530 if not missing_names and not unexpected_names:
    531     message += (
    532         "Feature names must be in the same order as they were in fit.\n"
    533     )
--> 535 raise ValueError(message)

ValueError: The feature names should match those that were passed during fit.
Feature names seen at fit time, yet now missing:
- diagonal_length_ratio
- height_ratio
- margin_ratio
In [60]:
# Exemple d'utilisation
if __name__ == "__main__":
    data_path = 'billets_production.csv'  # Remplacez par le chemin de votre fichier de données
    predictions = predict_banknotes(data_path)
Données chargées : 5 lignes
Une erreur s'est produite : The feature names should match those that were passed during fit.
Feature names seen at fit time, yet now missing:
- diagonal_length_ratio
- height_ratio
- margin_ratio

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[60], line 4
      2 if __name__ == "__main__":
      3     data_path = 'billets_production.csv'  # Remplacez par le chemin de votre fichier de données
----> 4     predictions = predict_banknotes(data_path)

Cell In[59], line 31, in predict_banknotes(data_path)
     29 # Charger le scaler et normaliser les données
     30 scaler = joblib.load(os.path.join('models', 'scaler.pkl'))
---> 31 X_scaled = scaler.transform(X)
     33 # Déterminer le meilleur modèle
     34 model_files = [f for f in os.listdir('models') if os.path.isfile(os.path.join('models', f))]

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\utils\_set_output.py:316, in _wrap_method_output.<locals>.wrapped(self, X, *args, **kwargs)
    314 @wraps(f)
    315 def wrapped(self, X, *args, **kwargs):
--> 316     data_to_wrap = f(self, X, *args, **kwargs)
    317     if isinstance(data_to_wrap, tuple):
    318         # only wrap the first output for cross decomposition
    319         return_tuple = (
    320             _wrap_data_with_container(method, data_to_wrap[0], X, self),
    321             *data_to_wrap[1:],
    322         )

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\preprocessing\_data.py:1045, in StandardScaler.transform(self, X, copy)
   1042 check_is_fitted(self)
   1044 copy = copy if copy is not None else self.copy
-> 1045 X = self._validate_data(
   1046     X,
   1047     reset=False,
   1048     accept_sparse="csr",
   1049     copy=copy,
   1050     dtype=FLOAT_DTYPES,
   1051     force_writeable=True,
   1052     force_all_finite="allow-nan",
   1053 )
   1055 if sparse.issparse(X):
   1056     if self.with_mean:

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\base.py:608, in BaseEstimator._validate_data(self, X, y, reset, validate_separately, cast_to_ndarray, **check_params)
    537 def _validate_data(
    538     self,
    539     X="no_validation",
   (...)
    544     **check_params,
    545 ):
    546     """Validate input data and set or check the `n_features_in_` attribute.
    547 
    548     Parameters
   (...)
    606         validated.
    607     """
--> 608     self._check_feature_names(X, reset=reset)
    610     if y is None and self._get_tags()["requires_y"]:
    611         raise ValueError(
    612             f"This {self.__class__.__name__} estimator "
    613             "requires y to be passed, but the target y is None."
    614         )

File c:\mamba\envs\Mamba_GPU_3_9\lib\site-packages\sklearn\base.py:535, in BaseEstimator._check_feature_names(self, X, reset)
    530 if not missing_names and not unexpected_names:
    531     message += (
    532         "Feature names must be in the same order as they were in fit.\n"
    533     )
--> 535 raise ValueError(message)

ValueError: The feature names should match those that were passed during fit.
Feature names seen at fit time, yet now missing:
- diagonal_length_ratio
- height_ratio
- margin_ratio
In [47]:
# Utilisation :
predictions = predict_production('billets_production.csv', model_name='random_forest')
Données chargées : 5 lignes

Vérification des colonnes :
Colonnes attendues : ['diagonal', 'height_left', 'height_right', 'margin_low', 'margin_up', 'length']
Colonnes présentes : ['diagonal', 'height_left', 'height_right', 'margin_low', 'margin_up', 'length', 'id']

Création des features dérivées...

Dimensions des données avant scaling : (5, 9)
Dimensions des données après scaling : (5, 9)

=== RÉSUMÉ DES PRÉDICTIONS ===
Nombre total de billets : 5

Billets authentiques : 2 (40.0%)

Détail des billets authentiques :
 ID Probabilité_Authentique
A_4                  100.0%
A_5                  100.0%

Billets contrefaits : 3 (60.0%)

Détail des billets contrefaits :
 ID Probabilité_Authentique
A_1                   -0.0%
A_2                   -0.0%
A_3                   -0.0%

=== RÉSULTATS COMPLETS ===
 ID  Prédiction      Classe Probabilité_Authentique
A_1           0 Contrefaçon                   -0.0%
A_2           0 Contrefaçon                   -0.0%
A_3           0 Contrefaçon                   -0.0%
A_4           1 Authentique                  100.0%
A_5           1 Authentique                  100.0%
In [48]:
def load_and_prepare_data(df):
    """
    Charge et prépare les données avec standardisation et gestion des valeurs manquantes.
    """
    logging.info("Début du chargement et de la préparation des données")
    
    variables = ['diagonal', 'height_left', 'height_right', 'margin_low', 'margin_up', 'length']
    X = df[variables].compute()  # Convertir en pandas DataFrame
    y = df['is_genuine'].compute()
    
    # Division en train/test AVANT le prétraitement
    X_train_val, X_test, y_train_val, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )
    
    # Imputation sur l'ensemble d'entraînement
    imputer = SimpleImputer(strategy='mean')
    X_train_imputed = imputer.fit_transform(X_train_val)
    X_test_imputed = imputer.transform(X_test)
    
    # Standardisation
    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train_imputed)
    X_test_scaled = scaler.transform(X_test_imputed)
    
    # Sauvegarde des transformateurs
    joblib.dump(imputer, 'parametres/imputer.joblib')
    joblib.dump(scaler, 'parametres/scaler.joblib')
    
    return X_train_scaled, X_test_scaled, y_train_val, y_test

# Configuration des modèles avec des grilles de paramètres étendues
models_config = {
    'Random Forest': {
        'model': RandomForestClassifier(random_state=42, n_jobs=-1),
        'params': {
            'n_estimators': [500, 1000, 1500],
            'max_depth': [None, 30, 50],
            'max_features': ['sqrt', 'log2'],
            'criterion': ['gini', 'entropy']
        }
    },
    'KNN': {
        'model': KNeighborsClassifier(n_jobs=-1),
        'params': {
            'n_neighbors': [10, 20, 30],
            'weights': ['uniform', 'distance'],
            'algorithm': ['auto']
        }
    },
    'Logistic Regression': {
        'model': LogisticRegression(random_state=42, max_iter=10000, n_jobs=-1),
        'params': {
            'C': [0.1, 1, 10, 100],
            'penalty': ['l2'],
            'solver': ['lbfgs']
        }
    }
}

def optimize_model(model_name, model_config, X_train, y_train, X_val, y_val):
    """
    Optimise les modèles avec une grille de paramètres étendue.
    Utilise GridSearchCV de scikit-learn.
    """
    logging.info(f"Optimisation de {model_name}")
    start_time = time.time()
    
    try:
        # GridSearchCV avec scikit-learn
        search = GridSearchCV(
            estimator=model_config['model'],
            param_grid=model_config['params'],
            cv=5,       # Nombre de plis de validation croisée
            n_jobs=-1,  # Utilisation maximale des ressources
            scoring='accuracy',
            return_train_score=True  # Assurez-vous que les scores d'entraînement sont retournés
        )
        
        search.fit(X_train, y_train)
        best_model = search.best_estimator_
        best_params = search.best_params_
        
        training_time = time.time() - start_time
        
        # Sauvegarde du meilleur modèle
        joblib.dump(best_model, f'parametres/model_{model_name.lower().replace(" ", "_")}.joblib')
        
        # Évaluation sur le jeu de validation
        y_val_pred = best_model.predict(X_val)
        accuracy_val = accuracy_score(y_val, y_val_pred)
        recall_val = recall_score(y_val, y_val_pred)
        precision_val = precision_score(y_val, y_val_pred)
        f1_val = f1_score(y_val, y_val_pred)
        roc_auc_val = roc_auc_score(y_val, y_val_pred)
        
        # Enregistrement des scores de cross-validation
        cv_results = pd.DataFrame(search.cv_results_)
        cv_results.to_csv(f'parametres/{model_name}_cv_results.csv', index=False)
        
        # Vérification et obtention du meilleur score de cross-validation sur le training set
        if 'mean_train_score' in search.cv_results_:
            accuracy_train_cv = max(search.cv_results_['mean_train_score'])
        else:
            logging.error(f"'mean_train_score' not found in GridSearchCV results for {model_name}")
            accuracy_train_cv = None
        
        return best_model, best_params, training_time, accuracy_train_cv, accuracy_val, recall_val, precision_val, f1_val, roc_auc_val
        
    except Exception as e:
        logging.error(f"Erreur lors de l'optimisation de {model_name}: {str(e)}")
        return None, None, None, None, None, None, None, None, None

def main(df):
    """
    Fonction principale d'exécution
    """
    try:
        # Préparation des données
        X_train_val, X_test, y_train_val, y_test = load_and_prepare_data(df)
        
        # Division en train et validation
        X_train, X_val, y_train, y_val = train_test_split(
            X_train_val, y_train_val, test_size=0.25, random_state=42, stratify=y_train_val
        )
        
        results = {
            'models': {},
            'params': {},
            'scores': {}
        }
        
        # Tracker pour les métriques
        metrics_tracker = {
            'model': [],
            'accuracy_train_cv': [],
            'accuracy_val': [],
            'roc_auc_val': [],
            'accuracy_test': [],
            'recall_test': [],
            'precision_test': [],
            'f1_test': [],
            'roc_auc_test': [],
            'training_time': []
        }
        
        # Optimisation des modèles
        for model_name, config in models_config.items():
            best_model, best_params, training_time, accuracy_train_cv, accuracy_val, recall_val, precision_val, f1_val, roc_auc_val = optimize_model(
                model_name, config, X_train, y_train, X_val, y_val
            )
            
            if best_model is not None:
                results['models'][model_name] = best_model
                results['params'][model_name] = best_params
                
                # Évaluation sur le test set
                y_test_pred = best_model.predict(X_test)
                accuracy_test = accuracy_score(y_test, y_test_pred)
                recall_test = recall_score(y_test, y_test_pred)
                precision_test = precision_score(y_test, y_test_pred)
                f1_test = f1_score(y_test, y_test_pred)
                roc_auc_test = roc_auc_score(y_test, y_test_pred)
                
                # Enregistrement des scores
                results['scores'][model_name] = {
                    'accuracy_train_cv': accuracy_train_cv,
                    'accuracy_val': accuracy_val,
                    'roc_auc_val': roc_auc_val,
                    'accuracy_test': accuracy_test,
                    'recall_test': recall_test,
                    'precision_test': precision_test,
                    'f1_test': f1_test,
                    'roc_auc_test': roc_auc_test,
                    'training_time': training_time
                }
                
                # Mise à jour du tracker
                metrics_tracker['model'].append(model_name)
                metrics_tracker['accuracy_train_cv'].append(accuracy_train_cv)
                metrics_tracker['accuracy_val'].append(accuracy_val)
                metrics_tracker['roc_auc_val'].append(roc_auc_val)
                metrics_tracker['accuracy_test'].append(accuracy_test)
                metrics_tracker['recall_test'].append(recall_test)
                metrics_tracker['precision_test'].append(precision_test)
                metrics_tracker['f1_test'].append(f1_test)
                metrics_tracker['roc_auc_test'].append(roc_auc_test)
                metrics_tracker['training_time'].append(training_time)
        
        # Sauvegarde des résultats en excluant les modèles
        results_to_save = {
            'params': results['params'],
            'scores': results['scores']
        }
        
        with open('parametres/results.json', 'w') as f:
            json.dump(results_to_save, f, indent=4)
        
        # Conversion du tracker en DataFrame pour les visualisations
        metrics_df = pd.DataFrame(metrics_tracker)
        metrics_df.to_csv('parametres/metrics_tracking.csv', index=False)
        
        # Création des visualisations
        create_visualizations(metrics_df)
        
        return results, metrics_df
        
    except Exception as e:
        logging.error(f"Erreur dans main: {str(e)}")
        raise

def create_visualizations(metrics_df):
    """
    Crée des visualisations pour comparer les modèles.
    """
    sns.set(style="whitegrid")  # Utiliser le style seaborn pour les graphiques
    
    # Graphique des accuracies
    plt.figure(figsize=(12, 6))
    bar_width = 0.2
    index = np.arange(len(metrics_df['model']))
    
    plt.bar(index, metrics_df['accuracy_train_cv'], bar_width, label='Accuracy Train CV')
    plt.bar(index + bar_width, metrics_df['accuracy_val'], bar_width, label='Accuracy Validation')
    plt.bar(index + 2 * bar_width, metrics_df['accuracy_test'], bar_width, label='Accuracy Test')
    
    plt.xlabel('Modèle')
    plt.ylabel('Accuracy')
    plt.title('Comparaison des Accuracies')
    plt.xticks(index + bar_width, metrics_df['model'])
    plt.legend()
    plt.tight_layout()
    plt.savefig('parametres/accuracy_comparison.png')
    plt.show()
    
    # Graphique des autres métriques sur le test set
    plt.figure(figsize=(12, 6))
    x = np.arange(len(metrics_df['model']))
    bar_width = 0.15

    plt.bar(x, metrics_df['precision_test'], bar_width, label='Precision')
    plt.bar(x + bar_width, metrics_df['recall_test'], bar_width, label='Recall')
    plt.bar(x + 2 * bar_width, metrics_df['f1_test'], bar_width, label='F1 Score')
    plt.bar(x + 3 * bar_width, metrics_df['roc_auc_test'], bar_width, label='ROC AUC')

    plt.xlabel('Modèle')
    plt.ylabel('Score')
    plt.title('Métriques sur le Test Set')
    plt.xticks(x + 1.5 * bar_width, metrics_df['model'])
    plt.legend()
    plt.tight_layout()
    plt.savefig('parametres/test_metrics_comparison.png')
    plt.show()
    
    # Graphique des temps d'entraînement
    plt.figure(figsize=(8, 6))
    plt.bar(metrics_df['model'], metrics_df['training_time'], color='skyblue')
    plt.title("Temps d'entraînement des modèles")
    plt.xlabel("Modèle")
    plt.ylabel("Temps (secondes)")
    plt.grid(True)
    plt.tight_layout()
    plt.savefig('parametres/training_time.png')
    plt.show()

if __name__ == "__main__":
    # Création du dossier pour les résultats
    os.makedirs("parametres", exist_ok=True)
    
    # Lecture des données avec Dask
    df_dask = dd.read_csv("billets.csv", sep=";")
    
    logging.info(f"Taille totale des données: {len(df_dask)}")
    
    # Exécution principale
    start_time = time.time()
    results, metrics_df = main(df_dask)
    total_duration = time.time() - start_time
    
    # Affichage des résultats
    print("\nRésultats :")
    for model_name, scores in results['scores'].items():
        print(f"\n{model_name}:")
        print(f"Accuracy sur training CV: {scores['accuracy_train_cv']:.4f}")
        print(f"Accuracy sur validation: {scores['accuracy_val']:.4f}")
        print(f"ROC AUC sur validation: {scores['roc_auc_val']:.4f}")
        print(f"Accuracy sur test: {scores['accuracy_test']:.4f}")
        print(f"Recall sur test: {scores['recall_test']:.4f}")
        print(f"Precision sur test: {scores['precision_test']:.4f}")
        print(f"F1-score sur test: {scores['f1_test']:.4f}")
        print(f"ROC AUC sur test: {scores['roc_auc_test']:.4f}")
        print(f"Temps d'entraînement: {scores['training_time']:.2f} secondes")
    
    print(f"\nDurée totale de l'exécution: {total_duration:.2f} secondes")
2024-12-13 05:55:53,250 - INFO - Taille totale des données: 1500
2024-12-13 05:55:53,251 - INFO - Début du chargement et de la préparation des données
2024-12-13 05:55:56,617 - INFO - Optimisation de Random Forest
2024-12-13 05:56:55,098 - INFO - Optimisation de KNN
2024-12-13 05:56:55,244 - INFO - Optimisation de Logistic Regression
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
Résultats :

Random Forest:
Accuracy sur training CV: 1.0000
Accuracy sur validation: 0.9833
ROC AUC sur validation: 0.9750
Accuracy sur test: 0.9867
Recall sur test: 0.9950
Precision sur test: 0.9851
F1-score sur test: 0.9900
ROC AUC sur test: 0.9825
Temps d'entraînement: 57.84 secondes

KNN:
Accuracy sur training CV: 1.0000
Accuracy sur validation: 0.9800
ROC AUC sur validation: 0.9725
Accuracy sur test: 0.9900
Recall sur test: 0.9950
Precision sur test: 0.9900
F1-score sur test: 0.9925
ROC AUC sur test: 0.9875
Temps d'entraînement: 0.11 secondes

Logistic Regression:
Accuracy sur training CV: 0.9933
Accuracy sur validation: 0.9900
ROC AUC sur validation: 0.9850
Accuracy sur test: 0.9900
Recall sur test: 0.9950
Precision sur test: 0.9900
F1-score sur test: 0.9925
ROC AUC sur test: 0.9875
Temps d'entraînement: 0.15 secondes

Durée totale de l'exécution: 64.18 secondes